This commit is contained in:
naiba
2024-10-20 00:09:16 +08:00
parent 6cd243ea40
commit 843ecdaa33
528 changed files with 281 additions and 19449 deletions

View File

@@ -1,7 +1,6 @@
package controller
import (
"errors"
"fmt"
"net/http"
"strconv"
@@ -11,7 +10,6 @@ import (
"github.com/gorilla/websocket"
"github.com/hashicorp/go-uuid"
"github.com/jinzhu/copier"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/crypto/bcrypt"
"golang.org/x/sync/singleflight"
@@ -68,7 +66,7 @@ func (p *commonPage) issueViewPassword(c *gin.Context) {
err := c.ShouldBind(&vpf)
var hash []byte
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
// err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
}
if err == nil {
hash, err = bcrypt.GenerateFromPassword([]byte(vpf.Password), bcrypt.DefaultCost)
@@ -76,9 +74,9 @@ func (p *commonPage) issueViewPassword(c *gin.Context) {
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusOK,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "AnErrorEccurred",
}),
// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
// MessageID: "AnErrorEccurred",
// }),
Msg: err.Error(),
}, true)
c.Abort()
@@ -107,7 +105,7 @@ func (p *commonPage) service(c *gin.Context) {
}, nil
})
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/service"), mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
"Services": res.([]interface{})[0],
"CycleTransferStats": res.([]interface{})[1],
}))
@@ -255,9 +253,9 @@ func (cp *commonPage) home(c *gin.Context) {
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "SystemError",
}),
// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
// MessageID: "SystemError",
// }),
Msg: "服务器状态获取失败",
Link: "/",
Btn: "返回首页",
@@ -284,9 +282,9 @@ func (cp *commonPage) ws(c *gin.Context) {
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "NetworkError",
}),
// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
// MessageID: "NetworkError",
// }),
Msg: "Websocket协议切换失败",
Link: "/",
Btn: "返回首页",
@@ -332,9 +330,9 @@ func (cp *commonPage) terminal(c *gin.Context) {
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "NetworkError",
}),
// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
// MessageID: "NetworkError",
// }),
Msg: "Websocket协议切换失败",
Link: "/",
Btn: "返回首页",
@@ -394,9 +392,9 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "SystemError",
}),
// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
// MessageID: "SystemError",
// }),
Msg: "生成会话ID失败",
Link: "/server",
Btn: "返回重试",
@@ -462,9 +460,9 @@ func (cp *commonPage) fm(c *gin.Context) {
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "NetworkError",
}),
// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
// MessageID: "NetworkError",
// }),
Msg: "Websocket协议切换失败",
Link: "/",
Btn: "返回首页",
@@ -508,9 +506,9 @@ func (cp *commonPage) createFM(c *gin.Context) {
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "SystemError",
}),
// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
// MessageID: "SystemError",
// }),
Msg: "生成会话ID失败",
Link: "/server",
Btn: "返回重试",

View File

@@ -2,20 +2,14 @@ package controller
import (
"fmt"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"code.cloudfoundry.org/bytefmt"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
"github.com/hashicorp/go-uuid"
"github.com/nicksnyder/go-i18n/v2/i18n"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
@@ -24,11 +18,29 @@ import (
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/proto"
"github.com/naiba/nezha/resource"
"github.com/naiba/nezha/service/rpc"
"github.com/naiba/nezha/service/singleton"
)
// @title Swagger Example API
// @version 1.0
// @description This is a sample server celler server.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api/v1
// @securityDefinitions.basic BasicAuth
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
func ServeWeb(port uint) *http.Server {
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
@@ -39,16 +51,7 @@ func ServeWeb(port uint) *http.Server {
}
r.Use(natGateway)
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
tmpl := template.New("").Funcs(funcMap)
var err error
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
if err != nil {
panic(err)
}
tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl)
r.Use(mygin.RecordPath)
r.StaticFS("/static", http.FS(resource.StaticFS))
routers(r)
page404 := func(c *gin.Context) {
mygin.ShowErrorPage(c, mygin.ErrInfo{
@@ -71,6 +74,19 @@ func ServeWeb(port uint) *http.Server {
}
func routers(r *gin.Engine) {
authMiddleware, err := jwt.New(initParams())
if err != nil {
log.Fatal("JWT Error:" + err.Error())
}
// register middleware
r.Use(handlerMiddleWare(authMiddleware))
r.POST("/login", authMiddleware.LoginHandler)
auth := r.Group("/auth", authMiddleware.MiddlewareFunc())
auth.GET("/refresh_token", authMiddleware.RefreshHandler)
// 通用页面
cp := commonPage{r: r}
cp.serve()
@@ -88,206 +104,6 @@ func routers(r *gin.Engine) {
}
}
func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
ret := tmpl
themes, err := os.ReadDir("resource/template")
if err != nil {
log.Printf("NEZHA>> Error reading themes folder: %v", err)
return ret
}
for _, theme := range themes {
if !theme.IsDir() {
continue
}
themeDir := theme.Name()
if themeDir == "theme-custom" {
// for backward compatibility
// note: will remove this in future versions
ret = loadTemplates(ret, themeDir)
continue
}
if strings.HasPrefix(themeDir, "dashboard-") {
// load dashboard templates, ignore desc file
ret = loadTemplates(ret, themeDir)
continue
}
if !strings.HasPrefix(themeDir, "theme-") {
log.Printf("NEZHA>> Invalid theme name: %s", themeDir)
continue
}
descPath := filepath.Join("resource", "template", themeDir, "theme.json")
desc, err := os.ReadFile(filepath.Clean(descPath))
if err != nil {
log.Printf("NEZHA>> Error opening %s config: %v", themeDir, err)
continue
}
themeName, err := utils.GjsonGet(desc, "name")
if err != nil {
log.Printf("NEZHA>> Error opening %s config: not a valid description file", theme.Name())
continue
}
// load templates
ret = loadTemplates(ret, themeDir)
themeKey := strings.TrimPrefix(themeDir, "theme-")
model.Themes[themeKey] = themeName.String()
}
return ret
}
func loadTemplates(tmpl *template.Template, themeDir string) *template.Template {
// load templates
templatePath := filepath.Join("resource", "template", themeDir, "*.html")
t, err := tmpl.ParseGlob(templatePath)
if err != nil {
log.Printf("NEZHA>> Error parsing templates %s: %v", themeDir, err)
return tmpl
}
return t
}
var funcMap = template.FuncMap{
"tr": func(id string, dataAndCount ...interface{}) string {
conf := i18n.LocalizeConfig{
MessageID: id,
}
if len(dataAndCount) > 0 {
conf.TemplateData = dataAndCount[0]
}
if len(dataAndCount) > 1 {
conf.PluralCount = dataAndCount[1]
}
return singleton.Localizer.MustLocalize(&conf)
},
"toValMap": func(val interface{}) map[string]interface{} {
return map[string]interface{}{
"Value": val,
}
},
"tf": func(t time.Time) string {
return t.In(singleton.Loc).Format("01/02/2006 15:04:05")
},
"len": func(slice []interface{}) string {
return strconv.Itoa(len(slice))
},
"safe": func(s string) template.HTML {
return template.HTML(s) // #nosec
},
"tag": func(s string) template.HTML {
return template.HTML(`<` + s + `>`) // #nosec
},
"stf": func(s uint64) string {
return time.Unix(int64(s), 0).In(singleton.Loc).Format("01/02/2006 15:04")
},
"sf": func(duration uint64) string {
return time.Duration(time.Duration(duration) * time.Second).String()
},
"sft": func(future time.Time) string {
return time.Until(future).Round(time.Second).String()
},
"bf": func(b uint64) string {
return bytefmt.ByteSize(b)
},
"ts": func(s string) string {
return strings.TrimSpace(s)
},
"float32f": func(f float32) string {
return fmt.Sprintf("%.3f", f)
},
"divU64": func(a, b uint64) float32 {
if b == 0 {
if a > 0 {
return 100
}
return 0
}
if a == 0 {
// 这是从未在线的情况
return 0.00001 / float32(b) * 100
}
return float32(a) / float32(b) * 100
},
"div": func(a, b int) float32 {
if b == 0 {
if a > 0 {
return 100
}
return 0
}
if a == 0 {
// 这是从未在线的情况
return 0.00001 / float32(b) * 100
}
return float32(a) / float32(b) * 100
},
"addU64": func(a, b uint64) uint64 {
return a + b
},
"add": func(a, b int) int {
return a + b
},
"TransLeftPercent": func(a, b float64) (n float64) {
n, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", (100-(a/b)*100)), 64)
if n < 0 {
n = 0
}
return
},
"TransLeft": func(a, b uint64) string {
if a < b {
return "0B"
}
return bytefmt.ByteSize(a - b)
},
"TransClassName": func(a float64) string {
if a == 0 {
return "offline"
}
if a > 50 {
return "fine"
}
if a > 20 {
return "warning"
}
if a > 0 {
return "error"
}
return "offline"
},
"UintToFloat": func(a uint64) (n float64) {
n, _ = strconv.ParseFloat((strconv.FormatUint(a, 10)), 64)
return
},
"dayBefore": func(i int) string {
year, month, day := time.Now().Date()
today := time.Date(year, month, day, 0, 0, 0, 0, singleton.Loc)
return today.AddDate(0, 0, i-29).Format("01/02")
},
"className": func(percent float32) string {
if percent == 0 {
return ""
}
if percent > 95 {
return "good"
}
if percent > 80 {
return "warning"
}
return "danger"
},
"statusName": func(val float32) string {
return singleton.StatusCodeToString(singleton.GetStatusCode(val))
},
}
func natGateway(c *gin.Context) {
natConfig := singleton.GetNATConfigByDomain(c.Request.Host)
if natConfig == nil {

View File

@@ -5,7 +5,6 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
@@ -56,7 +55,7 @@ func (gp *guestPage) login(c *gin.Context) {
RegistrationLink = singleton.Conf.Oauth2.OidcRegisterURL
}
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/login", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
"LoginType": LoginType,
"RegistrationLink": RegistrationLink,
}))

View File

@@ -0,0 +1,96 @@
package controller
import (
"log"
"time"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
"github.com/naiba/nezha/model"
)
func initParams() *jwt.GinJWTMiddleware {
return &jwt.GinJWTMiddleware{
Realm: "test zone",
Key: []byte("secret key"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
IdentityKey: model.CtxKeyAuthorizedUser,
PayloadFunc: payloadFunc(),
IdentityHandler: identityHandler(),
Authenticator: authenticator(),
Authorizator: authorizator(),
Unauthorized: unauthorized(),
TokenLookup: "header: Authorization, query: token, cookie: jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
}
}
func handlerMiddleWare(authMiddleware *jwt.GinJWTMiddleware) gin.HandlerFunc {
return func(context *gin.Context) {
errInit := authMiddleware.MiddlewareInit()
if errInit != nil {
log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error())
}
}
}
func payloadFunc() func(data interface{}) jwt.MapClaims {
return func(data interface{}) jwt.MapClaims {
if v, ok := data.(*model.User); ok {
return jwt.MapClaims{
model.CtxKeyAuthorizedUser: v.Username,
}
}
return jwt.MapClaims{}
}
}
func identityHandler() func(c *gin.Context) interface{} {
return func(c *gin.Context) interface{} {
claims := jwt.ExtractClaims(c)
return &model.User{
Username: claims[model.CtxKeyAuthorizedUser].(string),
}
}
}
func authenticator() func(c *gin.Context) (interface{}, error) {
return func(c *gin.Context) (interface{}, error) {
var loginVals model.LoginRequest
if err := c.ShouldBind(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
userID := loginVals.Username
password := loginVals.Password
if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
return &model.User{
Username: userID,
}, nil
}
return nil, jwt.ErrFailedAuthentication
}
}
func authorizator() func(data interface{}, c *gin.Context) bool {
return func(data interface{}, c *gin.Context) bool {
if v, ok := data.(*model.User); ok && v.Username == "admin" {
return true
}
return false
}
}
func unauthorized() func(c *gin.Context, code int, message string) {
return func(c *gin.Context, code int, message string) {
c.JSON(code, model.CommonResponse{
Success: false,
Error: model.CommonError{
Code: model.ApiErrorUnauthorized,
},
})
}
}

View File

@@ -19,7 +19,6 @@ import (
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/proto"
"github.com/naiba/nezha/resource"
"github.com/naiba/nezha/service/singleton"
)
@@ -1050,22 +1049,6 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
return
}
if !utils.IsFileExists("resource/template/theme-"+sf.Theme+"/home.html") && !resource.IsTemplateFileExist("template/theme-"+sf.Theme+"/home.html") {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("前台主题文件异常:%s", sf.Theme),
})
return
}
if !utils.IsFileExists("resource/template/dashboard-"+sf.DashboardTheme+"/setting.html") && !resource.IsTemplateFileExist("template/dashboard-"+sf.DashboardTheme+"/setting.html") {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("后台主题文件异常:%s", sf.DashboardTheme),
})
return
}
singleton.Conf.Language = sf.Language
singleton.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification == "on"
singleton.Conf.EnablePlainIPInNotification = sf.EnablePlainIPInNotification == "on"
@@ -1093,8 +1076,6 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
})
return
}
// 更新系统语言
singleton.InitLocalizer()
// 更新DNS服务器
singleton.OnNameserverUpdate()
c.JSON(http.StatusOK, model.Response{

View File

@@ -7,7 +7,6 @@ import (
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/service/singleton"
"github.com/nicksnyder/go-i18n/v2/i18n"
)
type memberPage struct {
@@ -19,9 +18,9 @@ func (mp *memberPage) serve() {
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
MemberOnly: true,
IsPage: true,
Msg: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
Btn: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
Redirect: "/login",
// Msg: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
// Btn: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
Redirect: "/login",
}))
mr.GET("/server", mp.server)
mr.GET("/monitor", mp.monitor)
@@ -37,7 +36,7 @@ func (mp *memberPage) api(c *gin.Context) {
singleton.ApiLock.RLock()
defer singleton.ApiLock.RUnlock()
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/api", mygin.CommonEnvironment(c, gin.H{
"title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ApiManagement"}),
// "title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ApiManagement"}),
"Tokens": singleton.ApiTokenList,
}))
}
@@ -46,14 +45,14 @@ func (mp *memberPage) server(c *gin.Context) {
singleton.SortedServerLock.RLock()
defer singleton.SortedServerLock.RUnlock()
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/server", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServersManagement"}),
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServersManagement"}),
"Servers": singleton.SortedServerList,
}))
}
func (mp *memberPage) monitor(c *gin.Context) {
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/monitor", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesManagement"}),
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesManagement"}),
"Monitors": singleton.ServiceSentinelShared.Monitors(),
}))
}
@@ -62,7 +61,7 @@ func (mp *memberPage) cron(c *gin.Context) {
var crons []model.Cron
singleton.DB.Find(&crons)
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/cron", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ScheduledTasks"}),
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ScheduledTasks"}),
"Crons": crons,
}))
}
@@ -73,7 +72,7 @@ func (mp *memberPage) notification(c *gin.Context) {
var ar []model.AlertRule
singleton.DB.Find(&ar)
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/notification", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Notification"}),
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Notification"}),
"Notifications": nf,
"AlertRules": ar,
}))
@@ -83,7 +82,7 @@ func (mp *memberPage) ddns(c *gin.Context) {
var data []model.DDNSProfile
singleton.DB.Find(&data)
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/ddns", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}),
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}),
"DDNS": data,
"ProviderMap": model.ProviderMap,
"ProviderList": model.ProviderList,
@@ -94,14 +93,14 @@ func (mp *memberPage) nat(c *gin.Context) {
var data []model.NAT
singleton.DB.Find(&data)
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/nat", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "NAT"}),
"NAT": data,
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "NAT"}),
"NAT": data,
}))
}
func (mp *memberPage) setting(c *gin.Context) {
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
"Languages": model.Languages,
"DashboardThemes": model.DashboardThemes,
}))