Merge branch 'email-settings-UI' into email-settings-UI-old

This commit is contained in:
Arminas 2023-01-04 13:55:09 +02:00 committed by GitHub
commit 10fc8903e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 102 additions and 721 deletions

View file

@ -78,34 +78,6 @@ function renderClientList(data) {
}); });
} }
function renderUserList(data) {
$.each(data, function(index, obj) {
let clientStatusHtml = '>'
// render user html content
let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="user_${obj.username}">
<div class="info-box">
<div class="info-box-content">
<div class="btn-group">
<button type="button" class="btn btn-outline-primary btn-sm" data-toggle="modal" data-target="#modal_edit_user" data-username="${obj.username}">Edit</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-outline-danger btn-sm" data-toggle="modal"
data-target="#modal_remove_user" data-username="${obj.username}">Delete</button>
</div>
<hr>
<span class="info-box-text"><i class="fas fa-user"></i> ${obj.username}</span>
<span class="info-box-text"><i class="fas fa-terminal"></i> ${obj.admin? 'Administrator':'Manager'}</span>
</div>
</div>
</div>`
// add the user html elements to the list
$('#users-list').append(html);
});
}
function prettyDateTime(timeStr) { function prettyDateTime(timeStr) {
const dt = new Date(timeStr); const dt = new Date(timeStr);
const offsetMs = dt.getTimezoneOffset() * 60 * 1000; const offsetMs = dt.getTimezoneOffset() * 60 * 1000;

View file

@ -42,54 +42,39 @@ func LoginPage() echo.HandlerFunc {
// Login for signing in handler // Login for signing in handler
func Login(db store.IStore) echo.HandlerFunc { func Login(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
data := make(map[string]interface{}) user := new(model.User)
err := json.NewDecoder(c.Request().Body).Decode(&data) c.Bind(user)
if err != nil { dbuser, err := db.GetUser()
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
username := data["username"].(string)
password := data["password"].(string)
rememberMe := data["rememberMe"].(bool)
dbuser, err := db.GetUserByName(username)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot query user from DB"})
} }
userCorrect := subtle.ConstantTimeCompare([]byte(username), []byte(dbuser.Username)) == 1 userCorrect := subtle.ConstantTimeCompare([]byte(user.Username), []byte(dbuser.Username)) == 1
var passwordCorrect bool var passwordCorrect bool
if dbuser.PasswordHash != "" { if dbuser.PasswordHash != "" {
match, err := util.VerifyHash(dbuser.PasswordHash, password) match, err := util.VerifyHash(dbuser.PasswordHash, user.Password)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot verify password"})
} }
passwordCorrect = match passwordCorrect = match
} else { } else {
passwordCorrect = subtle.ConstantTimeCompare([]byte(password), []byte(dbuser.Password)) == 1 passwordCorrect = subtle.ConstantTimeCompare([]byte(user.Password), []byte(dbuser.Password)) == 1
} }
if userCorrect && passwordCorrect { if userCorrect && passwordCorrect {
// TODO: refresh the token // TODO: refresh the token
ageMax := 0
expiration := time.Now().Add(24 * time.Hour)
if rememberMe {
ageMax = 86400
expiration.Add(144 * time.Hour)
}
sess, _ := session.Get("session", c) sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{ sess.Options = &sessions.Options{
Path: util.BasePath, Path: util.BasePath,
MaxAge: ageMax, MaxAge: 86400,
HttpOnly: true, HttpOnly: true,
} }
// set session_token // set session_token
tokenUID := xid.New().String() tokenUID := xid.New().String()
sess.Values["username"] = dbuser.Username sess.Values["username"] = user.Username
sess.Values["admin"] = dbuser.Admin
sess.Values["session_token"] = tokenUID sess.Values["session_token"] = tokenUID
sess.Save(c.Request(), c.Response()) sess.Save(c.Request(), c.Response())
@ -97,7 +82,7 @@ func Login(db store.IStore) echo.HandlerFunc {
cookie := new(http.Cookie) cookie := new(http.Cookie)
cookie.Name = "session_token" cookie.Name = "session_token"
cookie.Value = tokenUID cookie.Value = tokenUID
cookie.Expires = expiration cookie.Expires = time.Now().Add(24 * time.Hour)
c.SetCookie(cookie) c.SetCookie(cookie)
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"}) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Logged in successfully"})
@ -107,40 +92,6 @@ func Login(db store.IStore) echo.HandlerFunc {
} }
} }
// GetUsers handler return a JSON list of all users
func GetUsers(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
usersList, err := db.GetUsers()
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
false, fmt.Sprintf("Cannot get user list: %v", err),
})
}
return c.JSON(http.StatusOK, usersList)
}
}
// GetUser handler returns a JSON object of single user
func GetUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
username := c.Param("username")
if !isAdmin(c) && (username != currentUser(c)) {
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"})
}
userData, err := db.GetUserByName(username)
if err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "User not found"})
}
return c.JSON(http.StatusOK, userData)
}
}
// Logout to log a user out // Logout to log a user out
func Logout() echo.HandlerFunc { func Logout() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -152,23 +103,21 @@ func Logout() echo.HandlerFunc {
// LoadProfile to load user information // LoadProfile to load user information
func LoadProfile(db store.IStore) echo.HandlerFunc { func LoadProfile(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
userInfo, err := db.GetUser()
if err != nil {
log.Error("Cannot get user information: ", err)
}
return c.Render(http.StatusOK, "profile.html", map[string]interface{}{ return c.Render(http.StatusOK, "profile.html", map[string]interface{}{
"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c)},
"userInfo": userInfo,
}) })
} }
} }
// UsersSettings handler // UpdateProfile to update user information
func UsersSettings(db store.IStore) echo.HandlerFunc { func UpdateProfile(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
return c.Render(http.StatusOK, "users_settings.html", map[string]interface{}{
"baseData": model.BaseData{Active: "users-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)},
})
}
}
// UpdateUser to update user information
func UpdateUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
data := make(map[string]interface{}) data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data) err := json.NewDecoder(c.Request().Body).Decode(&data)
@ -179,18 +128,8 @@ func UpdateUser(db store.IStore) echo.HandlerFunc {
username := data["username"].(string) username := data["username"].(string)
password := data["password"].(string) password := data["password"].(string)
previousUsername := data["previous_username"].(string)
admin := data["admin"].(bool)
if !isAdmin(c) && (previousUsername != currentUser(c)) { user, err := db.GetUser()
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Manager cannot access other user data"})
}
if !isAdmin(c) {
admin = false
}
user, err := db.GetUserByName(previousUsername)
if err != nil { if err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()}) return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, err.Error()})
} }
@ -201,13 +140,6 @@ func UpdateUser(db store.IStore) echo.HandlerFunc {
user.Username = username user.Username = username
} }
if username != previousUsername {
_, err := db.GetUserByName(username)
if err == nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"})
}
}
if password != "" { if password != "" {
hash, err := util.HashPassword(password) hash, err := util.HashPassword(password)
if err != nil { if err != nil {
@ -216,96 +148,12 @@ func UpdateUser(db store.IStore) echo.HandlerFunc {
user.PasswordHash = hash user.PasswordHash = hash
} }
if previousUsername != currentUser(c) {
user.Admin = admin
}
if err := db.DeleteUser(previousUsername); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
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()})
} }
log.Infof("Updated user information successfully") log.Infof("Updated admin user information successfully")
if previousUsername == currentUser(c) { return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated admin user information successfully"})
setUser(c, user.Username, user.Admin)
}
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"})
}
}
// CreateUser to create new user
func CreateUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data)
if err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
var user model.User
username := data["username"].(string)
password := data["password"].(string)
admin := data["admin"].(bool)
if username == "" {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
} else {
user.Username = username
}
{
_, err := db.GetUserByName(username)
if err == nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "This username is taken"})
}
}
hash, err := util.HashPassword(password)
if err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
user.PasswordHash = hash
user.Admin = admin
if err := db.SaveUser(user); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
log.Infof("Created user successfully")
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Created user successfully"})
}
}
// RemoveUser handler
func RemoveUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
data := make(map[string]interface{})
err := json.NewDecoder(c.Request().Body).Decode(&data)
if err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
username := data["username"].(string)
if username == currentUser(c) {
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "User cannot delete itself"})
}
// delete user from database
if err := db.DeleteUser(username); err != nil {
log.Error("Cannot delete user: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot delete user from database"})
}
log.Infof("Removed user: %s", username)
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "User removed"})
} }
} }
@ -321,7 +169,7 @@ func WireGuardClients(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "clients.html", map[string]interface{}{ return c.Render(http.StatusOK, "clients.html", map[string]interface{}{
"baseData": model.BaseData{Active: "", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "baseData": model.BaseData{Active: "", CurrentUser: currentUser(c)},
"clientDataList": clientDataList, "clientDataList": clientDataList,
}) })
} }
@ -683,7 +531,7 @@ func WireGuardServer(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "server.html", map[string]interface{}{ return c.Render(http.StatusOK, "server.html", map[string]interface{}{
"baseData": model.BaseData{Active: "wg-server", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "baseData": model.BaseData{Active: "wg-server", CurrentUser: currentUser(c)},
"serverInterface": server.Interface, "serverInterface": server.Interface,
"serverKeyPair": server.KeyPair, "serverKeyPair": server.KeyPair,
}) })
@ -751,7 +599,7 @@ func GlobalSettings(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{ return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{
"baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c)},
"globalSettings": globalSettings, "globalSettings": globalSettings,
}) })
} }
@ -837,7 +685,7 @@ func Status(db store.IStore) echo.HandlerFunc {
wgClient, err := wgctrl.New() wgClient, err := wgctrl.New()
if err != nil { if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)},
"error": err.Error(), "error": err.Error(),
"devices": nil, "devices": nil,
}) })
@ -846,7 +694,7 @@ func Status(db store.IStore) echo.HandlerFunc {
devices, err := wgClient.Devices() devices, err := wgClient.Devices()
if err != nil { if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)},
"error": err.Error(), "error": err.Error(),
"devices": nil, "devices": nil,
}) })
@ -858,7 +706,7 @@ func Status(db store.IStore) echo.HandlerFunc {
clients, err := db.GetClients(false) clients, err := db.GetClients(false)
if err != nil { if err != nil {
return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{ return c.Render(http.StatusInternalServerError, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)},
"error": err.Error(), "error": err.Error(),
"devices": nil, "devices": nil,
}) })
@ -895,7 +743,7 @@ func Status(db store.IStore) echo.HandlerFunc {
} }
return c.Render(http.StatusOK, "status.html", map[string]interface{}{ return c.Render(http.StatusOK, "status.html", map[string]interface{}{
"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c)},
"devices": devicesVm, "devices": devicesVm,
"error": "", "error": "",
}) })
@ -1009,12 +857,6 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get client config"})
} }
users, err := db.GetUsers()
if err != nil {
log.Error("Cannot get users config: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get users config"})
}
settings, err := db.GetGlobalSettings() settings, err := db.GetGlobalSettings()
if err != nil { if err != nil {
log.Error("Cannot get global settings: ", err) log.Error("Cannot get global settings: ", err)
@ -1022,7 +864,7 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
} }
// Write config file // Write config file
err = util.WriteWireGuardServerConfig(tmplBox, server, clients, users, settings) err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings)
if err != nil { if err != nil {
log.Error("Cannot apply server config: ", err) log.Error("Cannot apply server config: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{

View file

@ -37,7 +37,7 @@ func GetWakeOnLanHosts(db store.IStore) echo.HandlerFunc {
} }
err = c.Render(http.StatusOK, "wake_on_lan_hosts.html", map[string]interface{}{ err = c.Render(http.StatusOK, "wake_on_lan_hosts.html", map[string]interface{}{
"baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c), Admin: isAdmin(c)}, "baseData": model.BaseData{Active: "wake_on_lan_hosts", CurrentUser: currentUser(c)},
"hosts": hosts, "hosts": hosts,
"error": "", "error": "",
}) })

View file

@ -14,24 +14,15 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
if !isValidSession(c) { if !isValidSession(c) {
nextURL := c.Request().URL nextURL := c.Request().URL
if nextURL != nil && c.Request().Method == http.MethodGet { if nextURL != nil && c.Request().Method == http.MethodGet {
return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath+"/login?next=%s", c.Request().URL)) return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath + "/login?next=%s", c.Request().URL))
} else { } else {
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login") return c.Redirect(http.StatusTemporaryRedirect, util.BasePath + "/login")
} }
} }
return next(c) return next(c)
} }
} }
func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if !isAdmin(c) {
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/")
}
return next(c)
}
}
func isValidSession(c echo.Context) bool { func isValidSession(c echo.Context) bool {
if util.DisableLogin { if util.DisableLogin {
return true return true
@ -55,29 +46,10 @@ func currentUser(c echo.Context) string {
return username return username
} }
// isAdmin to get user type: admin or manager
func isAdmin(c echo.Context) bool {
if util.DisableLogin {
return true
}
sess, _ := session.Get("session", c)
admin := fmt.Sprintf("%t", sess.Values["admin"])
return admin == "true"
}
func setUser(c echo.Context, username string, admin bool) {
sess, _ := session.Get("session", c)
sess.Values["username"] = username
sess.Values["admin"] = admin
sess.Save(c.Request(), c.Response())
}
// clearSession to remove current session // clearSession to remove current session
func clearSession(c echo.Context) { func clearSession(c echo.Context) {
sess, _ := session.Get("session", c) sess, _ := session.Get("session", c)
sess.Values["username"] = "" sess.Values["username"] = ""
sess.Values["admin"] = false
sess.Values["session_token"] = "" sess.Values["session_token"] = ""
sess.Save(c.Request(), c.Response()) sess.Save(c.Request(), c.Response())
} }

28
main.go
View file

@ -157,12 +157,7 @@ func main() {
app.POST(util.BasePath+"/login", handler.Login(db)) app.POST(util.BasePath+"/login", handler.Login(db))
app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession) app.GET(util.BasePath+"/logout", handler.Logout(), handler.ValidSession)
app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession) app.GET(util.BasePath+"/profile", handler.LoadProfile(db), handler.ValidSession)
app.GET(util.BasePath+"/users-settings", handler.UsersSettings(db), handler.ValidSession, handler.NeedsAdmin) app.POST(util.BasePath+"/profile", handler.UpdateProfile(db), handler.ValidSession)
app.POST(util.BasePath+"/update-user", handler.UpdateUser(db), handler.ValidSession)
app.POST(util.BasePath+"/create-user", handler.CreateUser(db), handler.ValidSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/remove-user", handler.RemoveUser(db), handler.ValidSession, handler.NeedsAdmin)
app.GET(util.BasePath+"/getusers", handler.GetUsers(db), handler.ValidSession, handler.NeedsAdmin)
app.GET(util.BasePath+"/api/user/:username", handler.GetUser(db), handler.ValidSession)
} }
app.GET(util.BasePath+"/_health", handler.Health()) app.GET(util.BasePath+"/_health", handler.Health())
@ -172,13 +167,13 @@ 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.NeedsAdmin) app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession)
app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.NeedsAdmin) app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession)
app.GET(util.BasePath+"/email-settings", handler.EmailSettings(db), handler.ValidSession, handler.NeedsAdmin) app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/email-settings", handler.EmailSettingsSubmit(db), handler.ValidSession, handler.NeedsAdmin) app.GET(util.BasePath+"/email-settings", handler.EmailSettings(db), handler.ValidSession)
app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.POST(util.BasePath+"/email-settings", handler.EmailSettingsSubmit(db), handler.ValidSession)
app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession) app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession)
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)
@ -217,13 +212,8 @@ func initServerConfig(db store.IStore, tmplBox *rice.Box) {
log.Fatalf("Cannot get client config: ", err) log.Fatalf("Cannot get client config: ", err)
} }
users, err := db.GetUsers()
if err != nil {
log.Fatalf("Cannot get user config: ", err)
}
// write config file // write config file
err = util.WriteWireGuardServerConfig(tmplBox, server, clients, users, settings) err = util.WriteWireGuardServerConfig(tmplBox, server, clients, settings)
if err != nil { if err != nil {
log.Fatalf("Cannot create server config: ", err) log.Fatalf("Cannot create server config: ", err)
} }

View file

@ -10,5 +10,4 @@ type Interface struct {
type BaseData struct { type BaseData struct {
Active string Active string
CurrentUser string CurrentUser string
Admin bool
} }

View file

@ -6,5 +6,4 @@ type User struct {
Password string `json:"password"` Password string `json:"password"`
// PasswordHash takes precedence over Password. // PasswordHash takes precedence over Password.
PasswordHash string `json:"password_hash"` PasswordHash string `json:"password_hash"`
Admin bool `json:"admin"`
} }

View file

@ -88,11 +88,6 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
log.Fatal(err) log.Fatal(err)
} }
tmplUsersSettingsString, err := tmplBox.String("users_settings.html")
if err != nil {
log.Fatal(err)
}
tmplStatusString, err := tmplBox.String("status.html") tmplStatusString, err := tmplBox.String("status.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -114,7 +109,6 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString)) 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["email_settings.html"] = template.Must(template.New("email_settings").Funcs(funcs).Parse(tmplBaseString + tmplEmailSettingsString)) templates["email_settings.html"] = template.Must(template.New("email_settings").Funcs(funcs).Parse(tmplBaseString + tmplEmailSettingsString))
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))
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))

View file

@ -43,7 +43,8 @@ func (o *JsonDB) Init() error {
var serverKeyPairPath string = path.Join(serverPath, "keypair.json") var serverKeyPairPath string = path.Join(serverPath, "keypair.json")
var globalSettingPath string = path.Join(serverPath, "global_settings.json") var globalSettingPath string = path.Join(serverPath, "global_settings.json")
var emailSettingPath string = path.Join(serverPath, "email_settings.json") var emailSettingPath string = path.Join(serverPath, "email_settings.json")
var userPath string = path.Join(o.dbPath, "users") var userPath string = path.Join(serverPath, "users.json")
// create directories if they do not exist // create directories if they do not exist
if _, err := os.Stat(clientPath); os.IsNotExist(err) { if _, err := os.Stat(clientPath); os.IsNotExist(err) {
os.MkdirAll(clientPath, os.ModePerm) os.MkdirAll(clientPath, os.ModePerm)
@ -54,9 +55,6 @@ func (o *JsonDB) Init() error {
if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) { if _, err := os.Stat(wakeOnLanHostsPath); os.IsNotExist(err) {
os.MkdirAll(wakeOnLanHostsPath, os.ModePerm) os.MkdirAll(wakeOnLanHostsPath, os.ModePerm)
} }
if _, err := os.Stat(userPath); os.IsNotExist(err) {
os.MkdirAll(userPath, os.ModePerm)
}
// server's interface // server's interface
if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) { if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) {
@ -128,11 +126,9 @@ func (o *JsonDB) Init() error {
} }
// user info // user info
results, err := o.conn.ReadAll("users") if _, err := os.Stat(userPath); os.IsNotExist(err) {
if err != nil || len(results) < 1 {
user := new(model.User) user := new(model.User)
user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername) user.Username = util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername)
user.Admin = util.DefaultIsAdmin
user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "") user.PasswordHash = util.LookupEnvOrString(util.PasswordHashEnvVar, "")
if user.PasswordHash == "" { if user.PasswordHash == "" {
plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword) plaintext := util.LookupEnvOrString(util.PasswordEnvVar, util.DefaultPassword)
@ -142,7 +138,7 @@ func (o *JsonDB) Init() error {
} }
user.PasswordHash = hash user.PasswordHash = hash
} }
o.conn.Write("users", user.Username, user) o.conn.Write("server", "users", user)
} }
return nil return nil
@ -154,44 +150,9 @@ func (o *JsonDB) GetUser() (model.User, error) {
return user, o.conn.Read("server", "users", &user) return user, o.conn.Read("server", "users", &user)
} }
// GetUsers func to get all users from the database // SaveUser func to user info to the database
func (o *JsonDB) GetUsers() ([]model.User, error) {
var users []model.User
results, err := o.conn.ReadAll("users")
if err != nil {
return users, err
}
for _, i := range results {
user := model.User{}
if err := json.Unmarshal([]byte(i), &user); err != nil {
return users, fmt.Errorf("cannot decode user json structure: %v", err)
}
users = append(users, user)
}
return users, err
}
// GetUserByName func to get single user from the database
func (o *JsonDB) GetUserByName(username string) (model.User, error) {
user := model.User{}
if err := o.conn.Read("users", username, &user); err != nil {
return user, err
}
return user, nil
}
// SaveUser func to save user in the database
func (o *JsonDB) SaveUser(user model.User) error { func (o *JsonDB) SaveUser(user model.User) error {
return o.conn.Write("users", user.Username, user) return o.conn.Write("server", "users", user)
}
// DeleteUser func to remove user from the database
func (o *JsonDB) DeleteUser(username string) error {
return o.conn.Delete("users", username)
} }
// GetGlobalSettings func to query global settings from the database // GetGlobalSettings func to query global settings from the database
@ -275,7 +236,7 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti
server, _ := o.GetServer() server, _ := o.GetServer()
globalSettings, _ := o.GetGlobalSettings() globalSettings, _ := o.GetGlobalSettings()
client := client client := client
if !qrCodeSettings.IncludeDNS { if !qrCodeSettings.IncludeDNS{
globalSettings.DNSServers = []string{} globalSettings.DNSServers = []string{}
} }
if !qrCodeSettings.IncludeMTU { if !qrCodeSettings.IncludeMTU {

View file

@ -6,10 +6,8 @@ import (
type IStore interface { type IStore interface {
Init() error Init() error
GetUsers() ([]model.User, error) GetUser() (model.User, error)
GetUserByName(username string) (model.User, error)
SaveUser(user model.User) error SaveUser(user model.User) error
DeleteUser(username string) error
GetGlobalSettings() (model.GlobalSetting, error) GetGlobalSettings() (model.GlobalSetting, error)
GetEmailSettings() (model.EmailSetting, error) GetEmailSettings() (model.EmailSetting, error)
GetServer() (model.Server, error) GetServer() (model.Server, error)

View file

@ -88,13 +88,7 @@
</div> </div>
<div class="info"> <div class="info">
{{if .baseData.CurrentUser}} {{if .baseData.CurrentUser}}
<a href="{{.basePath}}/profile" class="d-block">{{.baseData.CurrentUser}}</a>
{{if .baseData.Admin}}
<a href="{{.basePath}}/profile" class="d-block">Administrator: {{.baseData.CurrentUser}}</a>
{{else}}
<a href="{{.basePath}}/profile" class="d-block">Manager: {{.baseData.CurrentUser}}</a>
{{end}}
{{else}} {{else}}
<a href="#" class="d-block">Administrator</a> <a href="#" class="d-block">Administrator</a>
{{end}} {{end}}
@ -113,8 +107,6 @@
</p> </p>
</a> </a>
</li> </li>
{{if .baseData.Admin}}
<li class="nav-item"> <li class="nav-item">
<a href="{{.basePath}}/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}"> <a href="{{.basePath}}/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}">
<i class="nav-icon fas fa-server"></i> <i class="nav-icon fas fa-server"></i>
@ -123,8 +115,6 @@
</p> </p>
</a> </a>
</li> </li>
<li class="nav-header">SETTINGS</li> <li class="nav-header">SETTINGS</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}}">
@ -142,16 +132,6 @@
</p> </p>
</a> </a>
</li> </li>
<li class="nav-item">
<a href="{{.basePath}}/users-settings" class="nav-link {{if eq .baseData.Active "users-settings" }}active{{end}}">
<i class="nav-icon fas fa-cog"></i>
<p>
Users Settings
</p>
</a>
</li>
{{end}}
<li class="nav-header">UTILITIES</li> <li class="nav-header">UTILITIES</li>
<li class="nav-item"> <li class="nav-item">
<a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}"> <a href="{{.basePath}}/status" class="nav-link {{if eq .baseData.Active "status" }}active{{end}}">

View file

@ -99,11 +99,7 @@
$("#btn_login").click(function () { $("#btn_login").click(function () {
const username = $("#username").val(); const username = $("#username").val();
const password = $("#password").val(); const password = $("#password").val();
let rememberMe = false; const data = {"username": username, "password": password}
if ($("#remember").is(':checked')){
rememberMe = true;
}
const data = {"username": username, "password": password, "rememberMe": rememberMe}
$.ajax({ $.ajax({
cache: false, cache: false,

View file

@ -31,7 +31,7 @@ Profile
<div class="form-group"> <div class="form-group">
<label for="username" class="control-label">Username</label> <label for="username" class="control-label">Username</label>
<input type="text" class="form-control" name="username" id="username" <input type="text" class="form-control" name="username" id="username"
value=""> value="{{ .userInfo.Username }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password" class="control-label">Password</label> <label for="password" class="control-label">Password</label>
@ -55,82 +55,56 @@ Profile
{{ define "bottom_js"}} {{ define "bottom_js"}}
<script> <script>
{ function updateUserInfo() {
var previous_username; const username = $("#username").val();
var admin; const password = $("#password").val();
} const data = {"username": username, "password": password};
$(document).ready(function () { $.ajax({
$.ajax({ cache: false,
cache: false, method: 'POST',
method: 'GET', url: '{{.basePath}}/profile',
url: '{{.basePath}}/api/user/{{.baseData.CurrentUser}}', dataType: 'json',
dataType: 'json', contentType: "application/json",
contentType: "application/json", data: JSON.stringify(data),
success: function (resp) { success: function (data) {
const user = resp; toastr.success("Updated admin user information successfully");
$("#username").val(user.username); },
previous_username = user.username; error: function (jqXHR, exception) {
admin = user.admin; const responseJson = jQuery.parseJSON(jqXHR.responseText);
}, toastr.error(responseJson['message']);
error: function (jqXHR, exception) { }
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}); });
}
$(document).ready(function () {
function updateUserInfo() { $.validator.setDefaults({
const username = $("#username").val(); submitHandler: function () {
const password = $("#password").val(); updateUserInfo();
const data = {"username": username, "password": password, "previous_username": previous_username, "admin":admin}; }
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/update-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Updated user information successfully");
location.reload();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
$(document).ready(function () {
$.validator.setDefaults({
submitHandler: function () {
updateUserInfo();
}
});
$("#frm_profile").validate({
rules: {
username: {
required: true
}
},
messages: {
username: {
required: "Please enter a username",
}
},
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');
}
});
}); });
$("#frm_profile").validate({
rules: {
username: {
required: true
}
},
messages: {
username: {
required: "Please enter a username",
}
},
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> </script>
{{ end }} {{ end }}

View file

@ -1,294 +0,0 @@
{{define "title"}}
Users Settings
{{end}}
{{define "top_css"}}
{{end}}
{{define "username"}}
{{ .username }}
{{end}}
{{define "page_title"}}
Users Settings
{{end}}
{{define "page_content"}}
<section class="content">
<div class="container-fluid">
<div class="row" id="users-list">
</div>
</div>
</section>
<div class="modal fade" id="modal_edit_user">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Edit User</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form name="frm_edit_user" id="frm_edit_user">
<div class="modal-body">
<div class="form-group" style="display:none">
<input type="text" style="display:none" class="form-control" id="_previous_user_name"
name="_previous_user_name">
</div>
<div class="form-group">
<label for="_user_name" class="control-label">Name</label>
<input type="text" class="form-control" id="_user_name" name="_user_name">
</div>
<div class="form-group">
<label for="_user_password" class="control-label">Password</label>
<input type="text" class="form-control" id="_user_password" name="_user_password" value=""
placeholder="Leave empty to keep the password unchanged">
</div>
<div class="form-group">
<div class="icheck-primary d-inline">
<input type="checkbox" id="_admin">
<label for="_admin">
Admin
</label>
</div>
</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 -->
<div class="modal fade" id="modal_remove_user">
<div class="modal-dialog">
<div class="modal-content bg-danger">
<div class="modal-header">
<h4 class="modal-title">Remove</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
</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="remove_user_confirm">Apply</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{{end}}
{{define "bottom_js"}}
<script>
function populateUsersList() {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/getusers',
dataType: 'json',
contentType: "application/json",
success: function (data) {
renderUserList(data);
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
</script>
<script>
// load user list
$(document).ready(function () {
populateUsersList();
let newUserHtml = '<div class="col-sm-2 offset-md-4" style=" text-align: right;">' +
'<button style="" id="btn_new_user" type="button" class="btn btn-outline-primary btn-sm" ' +
'data-toggle="modal" data-target="#modal_edit_user" data-username="">' +
'<i class="nav-icon fas fa-plus"></i> New User</button></div>';
$('h1').parents(".row").append(newUserHtml);
})
// modal_remove_user modal event
$("#modal_remove_user").on('show.bs.modal', function (event) {
const button = $(event.relatedTarget);
const user_name = button.data('username');
const modal = $(this);
modal.find('.modal-body').text("You are about to remove user " + user_name);
modal.find('#remove_user_confirm').val(user_name);
})
// remove_user_confirm button event
$(document).ready(function () {
$("#remove_user_confirm").click(function () {
const user_name = $(this).val();
const data = {"username": user_name};
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/remove-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
$("#modal_remove_user").modal('hide');
toastr.success('Removed user successfully');
const divElement = document.getElementById('user_' + user_name);
divElement.style.display = "none";
location.reload()
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
});
// Edit user modal event
$(document).ready(function () {
$("#modal_edit_user").on('show.bs.modal', function (event) {
let modal = $(this);
const button = $(event.relatedTarget);
const user_name = button.data('username');
// update user modal data
if (user_name !== "") {
$.ajax({
cache: false,
method: 'GET',
url: '{{.basePath}}/api/user/' + user_name,
dataType: 'json',
contentType: "application/json",
success: function (resp) {
const user = resp;
modal.find(".modal-title").text("Edit user " + user.username);
modal.find("#_user_name").val(user.username);
modal.find("#_previous_user_name").val(user.username);
modal.find("#_user_password").val("");
modal.find("#_user_password").prop("placeholder", "Leave empty to keep the password unchanged")
modal.find("#_admin").prop("checked", user.admin);
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
} else {
modal.find(".modal-title").text("Add new user");
modal.find("#_user_name").val("");
modal.find("#_previous_user_name").val("");
modal.find("#_user_password").val("");
modal.find("#_user_password").prop("placeholder", "")
modal.find("#_admin").prop("checked", false);
}
});
});
function updateUserInfo() {
const username = $("#_user_name").val();
const previous_username = $("#_previous_user_name").val();
const password = $("#_user_password").val();
let admin = false;
if ($("#_admin").is(':checked')) {
admin = true;
}
const data = {
"username": username,
"password": password,
"previous_username": previous_username,
"admin": admin
};
if (previous_username !== "") {
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/update-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Updated user information successfully");
location.reload();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
} else {
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/create-user',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
toastr.success("Created user 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) {
updateUserInfo();
}
});
// Edit user form validation
$("#frm_edit_user").validate({
rules: {
_user_name: {
required: true
},
_user_password: {
required: function () {
return $("#_previous_user_name").val() === "";
}
},
},
messages: {
_user_name: {
required: "Please enter a username"
},
_user_password: {
required: "Please input a password"
},
},
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}}

View file

@ -24,7 +24,6 @@ var (
const ( const (
DefaultUsername = "admin" DefaultUsername = "admin"
DefaultPassword = "admin" DefaultPassword = "admin"
DefaultIsAdmin = true
DefaultServerAddress = "10.252.1.0/24" DefaultServerAddress = "10.252.1.0/24"
DefaultServerPort = 51820 DefaultServerPort = 51820
DefaultDNS = "1.1.1.1" DefaultDNS = "1.1.1.1"

View file

@ -381,7 +381,7 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
} }
// WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf // WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error { func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, clientDataList []model.ClientData, globalSettings model.GlobalSetting) error {
var tmplWireguardConf string var tmplWireguardConf string
// if set, read wg.conf template from WgConfTemplate // if set, read wg.conf template from WgConfTemplate
@ -416,7 +416,6 @@ func WriteWireGuardServerConfig(tmplBox *rice.Box, serverConfig model.Server, cl
"serverConfig": serverConfig, "serverConfig": serverConfig,
"clientDataList": clientDataList, "clientDataList": clientDataList,
"globalSettings": globalSettings, "globalSettings": globalSettings,
"usersList": usersList,
} }
err = t.Execute(f, config) err = t.Execute(f, config)