Added branding settings page
Added branding settings page, where you can change favicon, brand name and brand logo from UI.
This commit is contained in:
		
							parent
							
								
									46ac9ef2c1
								
							
						
					
					
						commit
						12d0dc6288
					
				| 
						 | 
					@ -5,8 +5,10 @@ import (
 | 
				
			||||||
	"encoding/base64"
 | 
						"encoding/base64"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
	"sort"
 | 
						"sort"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
| 
						 | 
					@ -33,11 +35,48 @@ func Health() echo.HandlerFunc {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Favicon() echo.HandlerFunc {
 | 
					func Favicon(db store.IStore) echo.HandlerFunc {
 | 
				
			||||||
	return func(c echo.Context) error {
 | 
						return func(c echo.Context) error {
 | 
				
			||||||
		if favicon, ok := os.LookupEnv(util.FaviconFilePathEnvVar); ok {
 | 
							if favicon, ok := os.LookupEnv(util.FaviconFilePathEnvVar); ok {
 | 
				
			||||||
			return c.File(favicon)
 | 
								return c.File(favicon)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								_, err := os.OpenFile(path.Join(db.GetPath(), "branding")+"/favicon.ico", os.O_RDONLY, 0777)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return c.Redirect(http.StatusFound, util.BasePath+"/static/custom/img/favicon.ico")
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								return c.File(path.Join(db.GetPath(), "branding") + "/favicon.ico")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func BrandLogo(db store.IStore) echo.HandlerFunc {
 | 
				
			||||||
 | 
						return func(c echo.Context) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_, err := os.OpenFile(path.Join(db.GetPath(), "branding")+"/logo.png", os.O_RDONLY, 0777)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return c.File(path.Join(db.GetPath(), "branding") + "/logo.png")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_, err = os.OpenFile(path.Join(db.GetPath(), "branding")+"/logo.jpg", os.O_RDONLY, 0777)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return c.File(path.Join(db.GetPath(), "branding") + "/logo.jpg")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_, err = os.OpenFile(path.Join(db.GetPath(), "branding")+"/logo.jpeg", os.O_RDONLY, 0777)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return c.File(path.Join(db.GetPath(), "branding") + "/logo.jpeg")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_, err = os.OpenFile(path.Join(db.GetPath(), "branding")+"/logo.gif", os.O_RDONLY, 0777)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return c.File(path.Join(db.GetPath(), "branding") + "/logo.gif")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_, err = os.OpenFile(path.Join(db.GetPath(), "branding")+"/logo.ico", os.O_RDONLY, 0777)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return c.File(path.Join(db.GetPath(), "branding") + "/logo.ico")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return c.Redirect(http.StatusFound, util.BasePath+"/static/custom/img/favicon.ico")
 | 
							return c.Redirect(http.StatusFound, util.BasePath+"/static/custom/img/favicon.ico")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -120,7 +159,7 @@ func LoadProfile(db store.IStore) echo.HandlerFunc {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		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)},
 | 
								"baseData": model.BaseData{Active: "profile", CurrentUser: currentUser(c), BrandName: db.GetBrandName()},
 | 
				
			||||||
			"userInfo": userInfo,
 | 
								"userInfo": userInfo,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -179,7 +218,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)},
 | 
								"baseData":       model.BaseData{Active: "", CurrentUser: currentUser(c), BrandName: db.GetBrandName()},
 | 
				
			||||||
			"clientDataList": clientDataList,
 | 
								"clientDataList": clientDataList,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -532,7 +571,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)},
 | 
								"baseData":        model.BaseData{Active: "wg-server", CurrentUser: currentUser(c), BrandName: db.GetBrandName()},
 | 
				
			||||||
			"serverInterface": server.Interface,
 | 
								"serverInterface": server.Interface,
 | 
				
			||||||
			"serverKeyPair":   server.KeyPair,
 | 
								"serverKeyPair":   server.KeyPair,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
| 
						 | 
					@ -600,7 +639,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)},
 | 
								"baseData":       model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), BrandName: db.GetBrandName()},
 | 
				
			||||||
			"globalSettings": globalSettings,
 | 
								"globalSettings": globalSettings,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -630,7 +669,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)},
 | 
									"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), BrandName: db.GetBrandName()},
 | 
				
			||||||
				"error":    err.Error(),
 | 
									"error":    err.Error(),
 | 
				
			||||||
				"devices":  nil,
 | 
									"devices":  nil,
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
| 
						 | 
					@ -639,7 +678,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)},
 | 
									"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), BrandName: db.GetBrandName()},
 | 
				
			||||||
				"error":    err.Error(),
 | 
									"error":    err.Error(),
 | 
				
			||||||
				"devices":  nil,
 | 
									"devices":  nil,
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
| 
						 | 
					@ -651,7 +690,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)},
 | 
										"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), BrandName: db.GetBrandName()},
 | 
				
			||||||
					"error":    err.Error(),
 | 
										"error":    err.Error(),
 | 
				
			||||||
					"devices":  nil,
 | 
										"devices":  nil,
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
| 
						 | 
					@ -697,7 +736,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)},
 | 
								"baseData": model.BaseData{Active: "status", CurrentUser: currentUser(c), BrandName: db.GetBrandName()},
 | 
				
			||||||
			"devices":  devicesVm,
 | 
								"devices":  devicesVm,
 | 
				
			||||||
			"error":    "",
 | 
								"error":    "",
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
| 
						 | 
					@ -831,11 +870,75 @@ func ApplyServerConfig(db store.IStore, tmplBox *rice.Box) echo.HandlerFunc {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// AboutPage handler
 | 
					// AboutPage handler
 | 
				
			||||||
func AboutPage() echo.HandlerFunc {
 | 
					func AboutPage(db store.IStore) echo.HandlerFunc {
 | 
				
			||||||
	return func(c echo.Context) error {
 | 
						return func(c echo.Context) error {
 | 
				
			||||||
		return c.Render(http.StatusOK, "about.html", map[string]interface{}{
 | 
							return c.Render(http.StatusOK, "about.html", map[string]interface{}{
 | 
				
			||||||
			"baseData": model.BaseData{Active: "about", CurrentUser: currentUser(c)},
 | 
								"baseData": model.BaseData{Active: "about", CurrentUser: currentUser(c), BrandName: db.GetBrandName()},
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Branding handler
 | 
				
			||||||
 | 
					func BrandingSettings(db store.IStore) echo.HandlerFunc {
 | 
				
			||||||
 | 
						return func(c echo.Context) error {
 | 
				
			||||||
 | 
							return c.Render(http.StatusOK, "branding_settings.html", map[string]interface{}{
 | 
				
			||||||
 | 
								"baseData": model.BaseData{Active: "branding-settings", CurrentUser: currentUser(c), BrandName: db.GetBrandName()},
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UpdateBranding(db store.IStore) echo.HandlerFunc {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return func(c echo.Context) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							c.Request().ParseMultipartForm(32 << 20)
 | 
				
			||||||
 | 
							brand_name := c.Request().Form.Get("brand_name")
 | 
				
			||||||
 | 
							favicon_exist := true
 | 
				
			||||||
 | 
							logo_exist := true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// check if favicon exist
 | 
				
			||||||
 | 
							favicon_file, favicon_handler, err := c.Request().FormFile("favicon")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								favicon_exist = false
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								defer favicon_file.Close()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// check if logo exist
 | 
				
			||||||
 | 
							logo_file, logo_handler, err := c.Request().FormFile("logo")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logo_exist = false
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								defer logo_file.Close()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if favicon_exist {
 | 
				
			||||||
 | 
								f, err := os.OpenFile(path.Join(db.GetPath(), "branding")+"/favicon."+strings.Split(favicon_handler.Filename, ".")[1], os.O_WRONLY|os.O_CREATE, 0777)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot open " + path.Join(db.GetPath(), "branding") + "/favicon." + strings.Split(favicon_handler.Filename, ".")[1]})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								defer f.Close()
 | 
				
			||||||
 | 
								io.Copy(f, favicon_file)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if logo_exist {
 | 
				
			||||||
 | 
								os.Remove(path.Join(db.GetPath(), "branding") + "/logo.png")
 | 
				
			||||||
 | 
								os.Remove(path.Join(db.GetPath(), "branding") + "/logo.jpg")
 | 
				
			||||||
 | 
								os.Remove(path.Join(db.GetPath(), "branding") + "/logo.jpeg")
 | 
				
			||||||
 | 
								os.Remove(path.Join(db.GetPath(), "branding") + "/logo.gif")
 | 
				
			||||||
 | 
								os.Remove(path.Join(db.GetPath(), "branding") + "/logo.ico")
 | 
				
			||||||
 | 
								f, err := os.OpenFile(path.Join(db.GetPath(), "branding")+"/logo."+strings.Split(logo_handler.Filename, ".")[1], os.O_WRONLY|os.O_CREATE, 0777)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot open " + path.Join(db.GetPath(), "branding") + "/logo." + strings.Split(logo_handler.Filename, ".")[1]})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								defer f.Close()
 | 
				
			||||||
 | 
								io.Copy(f, logo_file)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := db.SetBrandName(brand_name); err != nil {
 | 
				
			||||||
 | 
								return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot set brand name"})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated branding settings successfully"})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										7
									
								
								main.go
								
								
								
								
							
							
						
						
									
										7
									
								
								main.go
								
								
								
								
							| 
						 | 
					@ -147,9 +147,10 @@ func main() {
 | 
				
			||||||
		sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption)
 | 
							sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	app.GET(util.BasePath+"/about", handler.AboutPage())
 | 
						app.GET(util.BasePath+"/about", handler.AboutPage(db))
 | 
				
			||||||
	app.GET(util.BasePath+"/_health", handler.Health())
 | 
						app.GET(util.BasePath+"/_health", handler.Health())
 | 
				
			||||||
	app.GET(util.BasePath+"/favicon", handler.Favicon())
 | 
						app.GET(util.BasePath+"/favicon", handler.Favicon(db))
 | 
				
			||||||
 | 
						app.GET(util.BasePath+"/brand-logo", handler.BrandLogo(db))
 | 
				
			||||||
	app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson)
 | 
						app.POST(util.BasePath+"/new-client", handler.NewClient(db), handler.ValidSession, handler.ContentTypeJson)
 | 
				
			||||||
	app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson)
 | 
						app.POST(util.BasePath+"/update-client", handler.UpdateClient(db), handler.ValidSession, handler.ContentTypeJson)
 | 
				
			||||||
	app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson)
 | 
						app.POST(util.BasePath+"/email-client", handler.EmailClient(db, sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession, handler.ContentTypeJson)
 | 
				
			||||||
| 
						 | 
					@ -171,6 +172,8 @@ func main() {
 | 
				
			||||||
	app.POST(util.BasePath+"/wake_on_lan_host", handler.SaveWakeOnLanHost(db), handler.ValidSession, handler.ContentTypeJson)
 | 
						app.POST(util.BasePath+"/wake_on_lan_host", handler.SaveWakeOnLanHost(db), handler.ValidSession, handler.ContentTypeJson)
 | 
				
			||||||
	app.DELETE(util.BasePath+"/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession, handler.ContentTypeJson)
 | 
						app.DELETE(util.BasePath+"/wake_on_lan_host/:mac_address", handler.DeleteWakeOnHost(db), handler.ValidSession, handler.ContentTypeJson)
 | 
				
			||||||
	app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson)
 | 
						app.PUT(util.BasePath+"/wake_on_lan_host/:mac_address", handler.WakeOnHost(db), handler.ValidSession, handler.ContentTypeJson)
 | 
				
			||||||
 | 
						app.GET(util.BasePath+"/branding-settings", handler.BrandingSettings(db), handler.ValidSession)
 | 
				
			||||||
 | 
						app.POST(util.BasePath+"/update-branding", handler.UpdateBranding(db), handler.ValidSession)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// servers other static files
 | 
						// servers other static files
 | 
				
			||||||
	app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler)))
 | 
						app.GET(util.BasePath+"/static/*", echo.WrapHandler(http.StripPrefix(util.BasePath+"/static/", assetHandler)))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,4 +10,5 @@ type Interface struct {
 | 
				
			||||||
type BaseData struct {
 | 
					type BaseData struct {
 | 
				
			||||||
	Active      string
 | 
						Active      string
 | 
				
			||||||
	CurrentUser string
 | 
						CurrentUser string
 | 
				
			||||||
 | 
						BrandName   string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -98,6 +98,11 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
 | 
				
			||||||
		log.Fatal(err)
 | 
							log.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						brandingSettingsString, err := tmplBox.String("branding_settings.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,
 | 
				
			||||||
| 
						 | 
					@ -111,6 +116,7 @@ func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Ec
 | 
				
			||||||
	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["branding_settings.html"] = template.Must(template.New("branding_settings").Funcs(funcs).Parse(tmplBaseString + brandingSettingsString))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	e.Logger.SetLevel(log.DEBUG)
 | 
						e.Logger.SetLevel(log.DEBUG)
 | 
				
			||||||
	e.Pre(middleware.RemoveTrailingSlash())
 | 
						e.Pre(middleware.RemoveTrailingSlash())
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,6 +43,9 @@ 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 userPath string = path.Join(serverPath, "users.json")
 | 
						var userPath string = path.Join(serverPath, "users.json")
 | 
				
			||||||
 | 
						var brandingPath string = path.Join(o.dbPath, "branding")
 | 
				
			||||||
 | 
						var brandNamePath string = path.Join(brandingPath, "brand_name.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)
 | 
				
			||||||
| 
						 | 
					@ -53,6 +56,9 @@ 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(brandingPath); os.IsNotExist(err) {
 | 
				
			||||||
 | 
							os.MkdirAll(brandingPath, os.ModePerm)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// server's interface
 | 
						// server's interface
 | 
				
			||||||
	if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) {
 | 
						if _, err := os.Stat(serverInterfacePath); os.IsNotExist(err) {
 | 
				
			||||||
| 
						 | 
					@ -117,6 +123,14 @@ func (o *JsonDB) Init() error {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		o.conn.Write("server", "users", user)
 | 
							o.conn.Write("server", "users", user)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						// brand name
 | 
				
			||||||
 | 
						if _, err := os.Stat(brandNamePath); os.IsNotExist(err) {
 | 
				
			||||||
 | 
							type brandName struct {
 | 
				
			||||||
 | 
								BrandName string `json:"brand_name"`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							name := brandName{BrandName: "WIREGUARD UI"}
 | 
				
			||||||
 | 
							o.conn.Write("branding", "brand_name", name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -255,3 +269,30 @@ func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error {
 | 
				
			||||||
func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error {
 | 
					func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error {
 | 
				
			||||||
	return o.conn.Write("server", "global_settings", globalSettings)
 | 
						return o.conn.Write("server", "global_settings", globalSettings)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetBrandName func to get brand name from the database
 | 
				
			||||||
 | 
					func (o *JsonDB) GetBrandName() string {
 | 
				
			||||||
 | 
						type brandName struct {
 | 
				
			||||||
 | 
							BrandName string `json:"brand_name"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						name := brandName{}
 | 
				
			||||||
 | 
						o.conn.Read("branding", "brand_name", &name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := o.conn.Read("branding", "brand_name", &name); err != nil {
 | 
				
			||||||
 | 
							return "WIREGUARD UI"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return name.BrandName
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (o *JsonDB) SetBrandName(brandName string) error {
 | 
				
			||||||
 | 
						type brandNameStruct struct {
 | 
				
			||||||
 | 
							BrandName string `json:"brand_name"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						name := brandNameStruct{BrandName: brandName}
 | 
				
			||||||
 | 
						return o.conn.Write("branding", "brand_name", name)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (o *JsonDB) GetPath() string {
 | 
				
			||||||
 | 
						return o.dbPath
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,4 +22,7 @@ type IStore interface {
 | 
				
			||||||
	DeleteWakeOnHostLanHost(macAddress string) error
 | 
						DeleteWakeOnHostLanHost(macAddress string) error
 | 
				
			||||||
	SaveWakeOnLanHost(host model.WakeOnLanHost) error
 | 
						SaveWakeOnLanHost(host model.WakeOnLanHost) error
 | 
				
			||||||
	DeleteWakeOnHost(host model.WakeOnLanHost) error
 | 
						DeleteWakeOnHost(host model.WakeOnLanHost) error
 | 
				
			||||||
 | 
						GetBrandName() string
 | 
				
			||||||
 | 
						SetBrandName(brandName string) error
 | 
				
			||||||
 | 
						GetPath() string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,7 +78,9 @@
 | 
				
			||||||
        <aside class="main-sidebar sidebar-dark-primary elevation-4">
 | 
					        <aside class="main-sidebar sidebar-dark-primary elevation-4">
 | 
				
			||||||
            <!-- Brand Logo -->
 | 
					            <!-- Brand Logo -->
 | 
				
			||||||
            <a href="{{.basePath}}" class="brand-link">
 | 
					            <a href="{{.basePath}}" class="brand-link">
 | 
				
			||||||
                <span class="brand-text">  WIREGUARD UI</span>
 | 
					                <img src="{{.basePath}}/brand-logo" style="width: 100%">
 | 
				
			||||||
 | 
					                </br>
 | 
				
			||||||
 | 
					                <span class="brand-text">  {{.baseData.BrandName}}</span>
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <!-- Sidebar -->
 | 
					            <!-- Sidebar -->
 | 
				
			||||||
| 
						 | 
					@ -126,6 +128,14 @@
 | 
				
			||||||
                                </p>
 | 
					                                </p>
 | 
				
			||||||
                            </a>
 | 
					                            </a>
 | 
				
			||||||
                        </li>
 | 
					                        </li>
 | 
				
			||||||
 | 
					                        <li class="nav-item">
 | 
				
			||||||
 | 
					                            <a href="{{.basePath}}/branding-settings" class="nav-link {{if eq .baseData.Active "branding-settings" }}active{{end}}">
 | 
				
			||||||
 | 
					                            <i class="nav-icon fas fa-cog"></i>
 | 
				
			||||||
 | 
					                            <p>
 | 
				
			||||||
 | 
					                                Branding Settings
 | 
				
			||||||
 | 
					                            </p>
 | 
				
			||||||
 | 
					                            </a>
 | 
				
			||||||
 | 
					                        </li>
 | 
				
			||||||
                        <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}}">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,202 @@
 | 
				
			||||||
 | 
					{{define "title"}}
 | 
				
			||||||
 | 
					Branding Settings
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{define "top_css"}}
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{define "username"}}
 | 
				
			||||||
 | 
					{{ .username }}
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{define "page_title"}}
 | 
				
			||||||
 | 
					Branding Settings
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{define "page_content"}}
 | 
				
			||||||
 | 
					<section class="content">
 | 
				
			||||||
 | 
					    <div class="container-fluid">
 | 
				
			||||||
 | 
					        <!-- <h5 class="mt-4 mb-2">Global Settings</h5> -->
 | 
				
			||||||
 | 
					        <div class="row">
 | 
				
			||||||
 | 
					            <!-- left column -->
 | 
				
			||||||
 | 
					            <div class="col-md-6">
 | 
				
			||||||
 | 
					                <div class="card card-success">
 | 
				
			||||||
 | 
					                    <div class="card-header">
 | 
				
			||||||
 | 
					                        <h3 class="card-title">Wireguard-UI Branding Settings</h3>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <!-- /.card-header -->
 | 
				
			||||||
 | 
					                    <!-- form start -->
 | 
				
			||||||
 | 
					                    <form role="form" id="frm_branding_settings" name="frm_branding_settings">
 | 
				
			||||||
 | 
					                        <div class="card-body">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <div class="form-group">
 | 
				
			||||||
 | 
					                                <label for="favicon">Favicon</label>
 | 
				
			||||||
 | 
					                                <div class="input-group input-group">
 | 
				
			||||||
 | 
					                                    <input type="text" class="form-control" id="favicon" name="favicon" disabled>
 | 
				
			||||||
 | 
					                                    <span class="input-group-append">
 | 
				
			||||||
 | 
					                                        <label class="btn btn-default">Select favicon<input type="file"
 | 
				
			||||||
 | 
					                                                                                            id="favicon_input"
 | 
				
			||||||
 | 
					                                                                                            accept=".ico" hidden>
 | 
				
			||||||
 | 
					                                        </label>
 | 
				
			||||||
 | 
					                                    </span>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <div class="form-group">
 | 
				
			||||||
 | 
					                                <label for="brand_logo">Brand logo</label>
 | 
				
			||||||
 | 
					                                <div class="input-group input-group">
 | 
				
			||||||
 | 
					                                    <input type="text" class="form-control" id="brand_logo" name="brand_logo" disabled>
 | 
				
			||||||
 | 
					                                    <span class="input-group-append">
 | 
				
			||||||
 | 
					                                        <label class="btn btn-default">Select logo<input type="file"
 | 
				
			||||||
 | 
					                                                                                         id="brand_logo_input" accept=".png,.jpg,.jpeg,.gif,.ico" hidden>
 | 
				
			||||||
 | 
					                                        </label>
 | 
				
			||||||
 | 
					                                    </span>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <div class="form-group">
 | 
				
			||||||
 | 
					                                <label for="brand_name">Brand name</label>
 | 
				
			||||||
 | 
					                                <input type="text" class="form-control" id="brand_name"
 | 
				
			||||||
 | 
					                                       name="brand_name" placeholder="WIREGUARD UI"
 | 
				
			||||||
 | 
					                                       value="{{if .baseData.BrandName }}{{ .baseData.BrandName }}{{end}}">
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <!-- /.card-body -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <div class="card-footer">
 | 
				
			||||||
 | 
					                            <button type="submit" class="btn btn-success">Save</button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </form>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <!-- /.card -->
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="col-md-6">
 | 
				
			||||||
 | 
					                <div class="card card-success">
 | 
				
			||||||
 | 
					                    <div class="card-header">
 | 
				
			||||||
 | 
					                        <h3 class="card-title">Help</h3>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <!-- /.card-header -->
 | 
				
			||||||
 | 
					                    <div class="card-body">
 | 
				
			||||||
 | 
					                        <dl>
 | 
				
			||||||
 | 
					                            <dt>1. Favicon</dt>
 | 
				
			||||||
 | 
					                            <dd>Formats accepted: <code>.ico</code></dd>
 | 
				
			||||||
 | 
					                            <dd>Optional - leave blank or clear to continue using current favicon.</dd>
 | 
				
			||||||
 | 
					                            <dt>2. Brand logo</dt>
 | 
				
			||||||
 | 
					                            <dd>The logo, displayed in the upper left corner.</dd>
 | 
				
			||||||
 | 
					                            <dd>Formats accepted: <code>.png</code>, <code>.jpg</code>, <code>.jpeg</code>, <code>.gif</code>, <code>.ico</code></dd>
 | 
				
			||||||
 | 
					                            <dd>Optional - leave blank or clear to continue using current brand logo.</dd>
 | 
				
			||||||
 | 
					                            <dt>3. Brand name</dt>
 | 
				
			||||||
 | 
					                            <dd>The name, displayed in the upper left corner.</dd>
 | 
				
			||||||
 | 
					                            <dd>Mandatory - must contain at least one symbol.</dd>
 | 
				
			||||||
 | 
					                        </dl>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <!-- /.card -->
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <!-- /.row -->
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{{define "bottom_js"}}
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					    function submitBrandingSettings() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var data = new FormData();
 | 
				
			||||||
 | 
					        data.append("brand_name", $("#brand_name").val());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var count = 1;
 | 
				
			||||||
 | 
					        if (typeof document.getElementById("favicon_input").files[0] !== 'undefined') {
 | 
				
			||||||
 | 
					            readFile(data, 'favicon', "favicon_input");
 | 
				
			||||||
 | 
					            count++;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (typeof document.getElementById("brand_logo_input").files[0] !== 'undefined') {
 | 
				
			||||||
 | 
					            readFile(data, 'logo', "brand_logo_input");
 | 
				
			||||||
 | 
					            count++;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        function waitForRead() {
 | 
				
			||||||
 | 
					            var temp = 0;
 | 
				
			||||||
 | 
					            for (const pair of data.entries()) {
 | 
				
			||||||
 | 
					                temp++;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (temp !== count) {
 | 
				
			||||||
 | 
					                setTimeout(() => waitForRead(), 50);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                $.ajax({
 | 
				
			||||||
 | 
					                    url: "{{.basePath}}/update-branding",
 | 
				
			||||||
 | 
					                    data: data,
 | 
				
			||||||
 | 
					                    processData: false,
 | 
				
			||||||
 | 
					                    contentType: false,
 | 
				
			||||||
 | 
					                    type: "POST",
 | 
				
			||||||
 | 
					                    success: function(data) {
 | 
				
			||||||
 | 
					                        toastr.success('Updated branding successfully');
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    error: function(jqXHR, exception) {
 | 
				
			||||||
 | 
					                        const responseJson = jQuery.parseJSON(jqXHR.responseText);
 | 
				
			||||||
 | 
					                        toastr.error(responseJson['message']);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        waitForRead();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function readFile(data, name, element) {
 | 
				
			||||||
 | 
					        var image = document.getElementById(element).files[0];
 | 
				
			||||||
 | 
					        var reader = new FileReader();
 | 
				
			||||||
 | 
					        reader.readAsArrayBuffer(image);
 | 
				
			||||||
 | 
					        reader.onloadend = function () {
 | 
				
			||||||
 | 
					            data.append(name, image);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $("#favicon_input").on('change', function () {
 | 
				
			||||||
 | 
					        $("#favicon").val($("#favicon_input").val());
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $("#brand_logo_input").on('change', function () {
 | 
				
			||||||
 | 
					        $("#brand_logo").val($("#brand_logo_input").val());
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Form validation
 | 
				
			||||||
 | 
					    $(document).ready(function () {
 | 
				
			||||||
 | 
					        $.validator.setDefaults({
 | 
				
			||||||
 | 
					            submitHandler: function () {
 | 
				
			||||||
 | 
					                submitBrandingSettings();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        $("#frm_branding_settings").validate({
 | 
				
			||||||
 | 
					            rules: {
 | 
				
			||||||
 | 
					                brand_name: {
 | 
				
			||||||
 | 
					                    required: true
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            messages: {
 | 
				
			||||||
 | 
					                brand_name: {
 | 
				
			||||||
 | 
					                    required: "Please enter a brand name"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            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}}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue