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:
@@ -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
|
// UpdateUser to update user information
|
||||||
func UpdateUser(db store.IStore) echo.HandlerFunc {
|
func UpdateUser(db store.IStore) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
@@ -256,6 +265,19 @@ func UpdateUser(db store.IStore) echo.HandlerFunc {
|
|||||||
user.Admin = admin
|
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 {
|
if err := db.DeleteUser(previousUsername); err != nil {
|
||||||
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
|
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
|
||||||
}
|
}
|
||||||
@@ -308,6 +330,14 @@ func CreateUser(db store.IStore) echo.HandlerFunc {
|
|||||||
|
|
||||||
user.Admin = admin
|
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 {
|
if err := db.SaveUser(user); err != nil {
|
||||||
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
|
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
|
// NewClient handler
|
||||||
func NewClient(db store.IStore) echo.HandlerFunc {
|
func NewClient(db store.IStore) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
|
|||||||
@@ -251,7 +251,9 @@ func main() {
|
|||||||
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)
|
||||||
|
app.GET(util.BasePath+"/servers-settings", handler.ServersPage(), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin)
|
||||||
app.GET(util.BasePath+"/servers", handler.ListServers(db), handler.ValidSession)
|
app.GET(util.BasePath+"/servers", handler.ListServers(db), handler.ValidSession)
|
||||||
|
app.POST(util.BasePath+"/servers", handler.CreateServer(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
|
||||||
app.GET(util.BasePath+"/servers/:id/clients", handler.GetServerClients(db), handler.ValidSession, handler.RequireServerAccess(db))
|
app.GET(util.BasePath+"/servers/:id/clients", handler.GetServerClients(db), handler.ValidSession, handler.RequireServerAccess(db))
|
||||||
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)
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tmplServersString, err := util.StringFromEmbedFile(tmplDir, "servers.html")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// create template list
|
// create template list
|
||||||
funcs := template.FuncMap{
|
funcs := template.FuncMap{
|
||||||
"StringsJoin": strings.Join,
|
"StringsJoin": strings.Join,
|
||||||
@@ -125,6 +130,7 @@ func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo
|
|||||||
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))
|
||||||
templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString))
|
templates["wake_on_lan_hosts.html"] = template.Must(template.New("wake_on_lan_hosts").Funcs(funcs).Parse(tmplBaseString + tmplWakeOnLanHostsString))
|
||||||
templates["about.html"] = template.Must(template.New("about").Funcs(funcs).Parse(tmplBaseString + aboutPageString))
|
templates["about.html"] = template.Must(template.New("about").Funcs(funcs).Parse(tmplBaseString + aboutPageString))
|
||||||
|
templates["servers.html"] = template.Must(template.New("servers").Funcs(funcs).Parse(tmplBaseString + tmplServersString))
|
||||||
|
|
||||||
lvl, err := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO"))
|
lvl, err := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -139,6 +139,14 @@
|
|||||||
|
|
||||||
|
|
||||||
<li class="nav-header">SETTINGS</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}}">
|
||||||
|
<i class="nav-icon fas fa-server"></i>
|
||||||
|
<p>
|
||||||
|
Servers
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="{{.basePath}}/global-settings" class="nav-link {{if eq .baseData.Active "global-settings" }}active{{end}}">
|
<a href="{{.basePath}}/global-settings" class="nav-link {{if eq .baseData.Active "global-settings" }}active{{end}}">
|
||||||
<i class="nav-icon fas fa-cog"></i>
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
{{define "title"}}
|
||||||
|
Servers
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "top_css"}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "username"}}
|
||||||
|
{{ .username }}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "page_title"}}
|
||||||
|
Servers
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "page_content"}}
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row" id="servers-list">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="modal fade" id="modal_new_server">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">Add new server</h4>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form name="frm_new_server" id="frm_new_server">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="_server_id" class="control-label">ID</label>
|
||||||
|
<input type="text" class="form-control" id="_server_id" name="_server_id"
|
||||||
|
placeholder="e.g. wg-home">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="_server_name" class="control-label">Name</label>
|
||||||
|
<input type="text" class="form-control" id="_server_name" name="_server_name"
|
||||||
|
placeholder="e.g. WGhome">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="_server_interface" class="control-label">Interface</label>
|
||||||
|
<input type="text" class="form-control" id="_server_interface" name="_server_interface"
|
||||||
|
placeholder="e.g. wg-home">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="_server_addresses" class="control-label">Addresses</label>
|
||||||
|
<input type="text" class="form-control" id="_server_addresses" name="_server_addresses"
|
||||||
|
placeholder="e.g. 10.20.22.0/24, fd00::/64">
|
||||||
|
<small class="form-text text-muted">Comma-separated list of address ranges.</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="_server_listen_port" class="control-label">Listen Port</label>
|
||||||
|
<input type="text" class="form-control" id="_server_listen_port" name="_server_listen_port"
|
||||||
|
placeholder="e.g. 51822">
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
function renderServersList(data) {
|
||||||
|
$.each(data, function (index, 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 : "";
|
||||||
|
|
||||||
|
let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="server_${obj.id}">
|
||||||
|
<div class="info-box">
|
||||||
|
<div class="info-box-content">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{.basePath}}/servers/${obj.id}/clients" class="btn btn-outline-primary btn-sm">Manage clients</a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<span class="info-box-text"><i class="fas fa-server"></i> ${obj.name}</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-ethernet"></i> Interface: ${interfaceName}</span>
|
||||||
|
<span class="info-box-text"><i class="fas fa-plug"></i> Listen Port: ${listenPort}</span>
|
||||||
|
<span class="info-box-text"><i class="fas fa-map-marker-alt"></i> Addresses: ${addresses}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
|
||||||
|
$('#servers-list').append(html);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateServersList() {
|
||||||
|
$.ajax({
|
||||||
|
cache: false,
|
||||||
|
method: 'GET',
|
||||||
|
url: '{{.basePath}}/servers',
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: "application/json",
|
||||||
|
success: function (data) {
|
||||||
|
renderServersList(data);
|
||||||
|
},
|
||||||
|
error: function (jqXHR, exception) {
|
||||||
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||||
|
toastr.error(responseJson['message']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// load server list
|
||||||
|
$(document).ready(function () {
|
||||||
|
populateServersList();
|
||||||
|
let newServerHtml = '<div class="col-sm-2 offset-md-4" style=" text-align: right;">' +
|
||||||
|
'<button style="" id="btn_new_server" type="button" class="btn btn-outline-primary btn-sm" ' +
|
||||||
|
'data-toggle="modal" data-target="#modal_new_server">' +
|
||||||
|
'<i class="nav-icon fas fa-plus"></i> New Server</button></div>';
|
||||||
|
$('h1').parents(".row").append(newServerHtml);
|
||||||
|
})
|
||||||
|
|
||||||
|
// New server modal event: reset the form each time it is opened
|
||||||
|
$(document).ready(function () {
|
||||||
|
$("#modal_new_server").on('show.bs.modal', function (event) {
|
||||||
|
let modal = $(this);
|
||||||
|
modal.find("#_server_id").val("");
|
||||||
|
modal.find("#_server_name").val("");
|
||||||
|
modal.find("#_server_interface").val("");
|
||||||
|
modal.find("#_server_addresses").val("");
|
||||||
|
modal.find("#_server_listen_port").val("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function submitNewServer() {
|
||||||
|
const id = $("#_server_id").val();
|
||||||
|
const name = $("#_server_name").val();
|
||||||
|
const iface = $("#_server_interface").val();
|
||||||
|
const addresses = $("#_server_addresses").val().split(",").map(function (a) {
|
||||||
|
return a.trim();
|
||||||
|
}).filter(function (a) {
|
||||||
|
return a !== "";
|
||||||
|
});
|
||||||
|
const listen_port = parseInt($("#_server_listen_port").val(), 10);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"id": id,
|
||||||
|
"name": name,
|
||||||
|
"interface": iface,
|
||||||
|
"addresses": addresses,
|
||||||
|
"listen_port": listen_port
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
cache: false,
|
||||||
|
method: 'POST',
|
||||||
|
url: '{{.basePath}}/servers',
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: "application/json",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
success: function (data) {
|
||||||
|
$("#modal_new_server").modal('hide');
|
||||||
|
toastr.success("Created server successfully");
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
error: function (jqXHR, exception) {
|
||||||
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||||
|
toastr.error(responseJson['message']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$.validator.setDefaults({
|
||||||
|
submitHandler: function (form) {
|
||||||
|
submitNewServer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("#frm_new_server").validate({
|
||||||
|
rules: {
|
||||||
|
_server_id: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
_server_name: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
_server_interface: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
_server_listen_port: {
|
||||||
|
required: true,
|
||||||
|
digits: true,
|
||||||
|
range: [1, 65535]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
_server_id: {
|
||||||
|
required: "Please enter a server ID"
|
||||||
|
},
|
||||||
|
_server_name: {
|
||||||
|
required: "Please enter a server name"
|
||||||
|
},
|
||||||
|
_server_interface: {
|
||||||
|
required: "Please enter an interface name"
|
||||||
|
},
|
||||||
|
_server_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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
@@ -53,6 +53,12 @@ Users Settings
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="_server_ids" class="control-label">Server Access</label>
|
||||||
|
<select multiple class="form-control" id="_server_ids" name="_server_ids">
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">Servers this user (if non-admin) may access. Admins always have access to all servers.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer justify-content-between">
|
<div class="modal-footer justify-content-between">
|
||||||
@@ -163,6 +169,32 @@ Users Settings
|
|||||||
const button = $(event.relatedTarget);
|
const button = $(event.relatedTarget);
|
||||||
const user_name = button.data('username');
|
const user_name = button.data('username');
|
||||||
|
|
||||||
|
// populate the server access select with the current list of servers
|
||||||
|
$.ajax({
|
||||||
|
cache: false,
|
||||||
|
method: 'GET',
|
||||||
|
url: '{{.basePath}}/servers',
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: "application/json",
|
||||||
|
success: function (servers) {
|
||||||
|
const select = modal.find("#_server_ids");
|
||||||
|
select.empty();
|
||||||
|
$.each(servers, function (index, srv) {
|
||||||
|
select.append($('<option>').val(srv.id).text(srv.name + " (" + srv.id + ")"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// if editing an existing user, pre-select their granted servers now that
|
||||||
|
// the option list exists
|
||||||
|
if (user_name !== "") {
|
||||||
|
select.val(select.data('preselect') || []);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (jqXHR, exception) {
|
||||||
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||||
|
toastr.error(responseJson['message']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// update user modal data
|
// update user modal data
|
||||||
if (user_name !== "") {
|
if (user_name !== "") {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -180,6 +212,10 @@ Users Settings
|
|||||||
modal.find("#_user_password").val("");
|
modal.find("#_user_password").val("");
|
||||||
modal.find("#_user_password").prop("placeholder", "Leave empty to keep the password unchanged")
|
modal.find("#_user_password").prop("placeholder", "Leave empty to keep the password unchanged")
|
||||||
modal.find("#_admin").prop("checked", user.admin);
|
modal.find("#_admin").prop("checked", user.admin);
|
||||||
|
// remember the granted server ids so the select can pre-select them
|
||||||
|
// once its options have been populated (see the servers ajax above)
|
||||||
|
modal.find("#_server_ids").data('preselect', user.server_ids || []);
|
||||||
|
modal.find("#_server_ids").val(user.server_ids || []);
|
||||||
},
|
},
|
||||||
error: function (jqXHR, exception) {
|
error: function (jqXHR, exception) {
|
||||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||||
@@ -193,6 +229,7 @@ Users Settings
|
|||||||
modal.find("#_user_password").val("");
|
modal.find("#_user_password").val("");
|
||||||
modal.find("#_user_password").prop("placeholder", "")
|
modal.find("#_user_password").prop("placeholder", "")
|
||||||
modal.find("#_admin").prop("checked", false);
|
modal.find("#_admin").prop("checked", false);
|
||||||
|
modal.find("#_server_ids").data('preselect', []);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -205,11 +242,13 @@ Users Settings
|
|||||||
if ($("#_admin").is(':checked')) {
|
if ($("#_admin").is(':checked')) {
|
||||||
admin = true;
|
admin = true;
|
||||||
}
|
}
|
||||||
|
const server_ids = $("#_server_ids").val() || [];
|
||||||
const data = {
|
const data = {
|
||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
"previous_username": previous_username,
|
"previous_username": previous_username,
|
||||||
"admin": admin
|
"admin": admin,
|
||||||
|
"server_ids": server_ids
|
||||||
};
|
};
|
||||||
|
|
||||||
if (previous_username !== "") {
|
if (previous_username !== "") {
|
||||||
|
|||||||
Reference in New Issue
Block a user