From fd766e7b1a5bf392fb7892ace2732d0bbfcdfae0 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 16 Aug 2020 13:36:54 +0100 Subject: [PATCH] use app identifier instead of ctx changing this because ctx is commonly used with the context package. --- api.go | 349 +++++++++++++++++++++++++++-------------------------- auth.go | 53 ++++---- config.go | 45 +++---- daemon.go | 14 +-- email.go | 77 ++++++------ main.go | 253 +++++++++++++++++++------------------- pwreset.go | 47 ++++---- setup.go | 6 +- views.go | 53 ++++---- 9 files changed, 452 insertions(+), 445 deletions(-) diff --git a/api.go b/api.go index 07f051d..db0575c 100644 --- a/api.go +++ b/api.go @@ -2,33 +2,34 @@ package main import ( "fmt" - "github.com/gin-gonic/gin" - "github.com/knz/strtime" - "github.com/lithammer/shortuuid/v3" - "gopkg.in/ini.v1" "os" "os/signal" "syscall" "time" + + "github.com/gin-gonic/gin" + "github.com/knz/strtime" + "github.com/lithammer/shortuuid/v3" + "gopkg.in/ini.v1" ) -func (ctx *appContext) loadStrftime() { - ctx.datePattern = ctx.config.Section("email").Key("date_format").String() - ctx.timePattern = `%H:%M` - if val, _ := ctx.config.Section("email").Key("use_24h").Bool(); !val { - ctx.timePattern = `%I:%M %p` +func (app *appContext) loadStrftime() { + app.datePattern = app.config.Section("email").Key("date_format").String() + app.timePattern = `%H:%M` + if val, _ := app.config.Section("email").Key("use_24h").Bool(); !val { + app.timePattern = `%I:%M %p` } return } -func (ctx *appContext) prettyTime(dt time.Time) (date, time string) { - date, _ = strtime.Strftime(dt, ctx.datePattern) - time, _ = strtime.Strftime(dt, ctx.timePattern) +func (app *appContext) prettyTime(dt time.Time) (date, time string) { + date, _ = strtime.Strftime(dt, app.datePattern) + time, _ = strtime.Strftime(dt, app.timePattern) return } -func (ctx *appContext) formatDatetime(dt time.Time) string { - d, t := ctx.prettyTime(dt) +func (app *appContext) formatDatetime(dt time.Time) string { + d, t := app.prettyTime(dt) return d + " " + t } @@ -79,60 +80,60 @@ func timeDiff(a, b time.Time) (year, month, day, hour, min, sec int) { return } -func (ctx *appContext) checkInvites() { +func (app *appContext) checkInvites() { current_time := time.Now() - ctx.storage.loadInvites() + app.storage.loadInvites() changed := false - for code, data := range ctx.storage.invites { + for code, data := range app.storage.invites { expiry := data.ValidTill if current_time.After(expiry) { - ctx.debug.Printf("Housekeeping: Deleting old invite %s", code) + app.debug.Printf("Housekeeping: Deleting old invite %s", code) notify := data.Notify - if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { - ctx.debug.Printf("%s: Expiry notification", code) + if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { + app.debug.Printf("%s: Expiry notification", code) for address, settings := range notify { if settings["notify-expiry"] { go func() { - if ctx.email.constructExpiry(code, data, ctx) != nil { - ctx.err.Printf("%s: Failed to construct expiry notification", code) - } else if ctx.email.send(address, ctx) != nil { - ctx.err.Printf("%s: Failed to send expiry notification", code) + if app.email.constructExpiry(code, data, app) != nil { + app.err.Printf("%s: Failed to construct expiry notification", code) + } else if app.email.send(address, app) != nil { + app.err.Printf("%s: Failed to send expiry notification", code) } else { - ctx.info.Printf("Sent expiry notification to %s", address) + app.info.Printf("Sent expiry notification to %s", address) } }() } } } changed = true - delete(ctx.storage.invites, code) + delete(app.storage.invites, code) } } if changed { - ctx.storage.storeInvites() + app.storage.storeInvites() } } -func (ctx *appContext) checkInvite(code string, used bool, username string) bool { +func (app *appContext) checkInvite(code string, used bool, username string) bool { current_time := time.Now() - ctx.storage.loadInvites() + app.storage.loadInvites() changed := false - if inv, match := ctx.storage.invites[code]; match { + if inv, match := app.storage.invites[code]; match { expiry := inv.ValidTill if current_time.After(expiry) { - ctx.debug.Printf("Housekeeping: Deleting old invite %s", code) + app.debug.Printf("Housekeeping: Deleting old invite %s", code) notify := inv.Notify - if ctx.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { - ctx.debug.Printf("%s: Expiry notification", code) + if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { + app.debug.Printf("%s: Expiry notification", code) for address, settings := range notify { if settings["notify-expiry"] { go func() { - if ctx.email.constructExpiry(code, inv, ctx) != nil { - ctx.err.Printf("%s: Failed to construct expiry notification", code) - } else if ctx.email.send(address, ctx) != nil { - ctx.err.Printf("%s: Failed to send expiry notification", code) + if app.email.constructExpiry(code, inv, app) != nil { + app.err.Printf("%s: Failed to construct expiry notification", code) + } else if app.email.send(address, app) != nil { + app.err.Printf("%s: Failed to send expiry notification", code) } else { - ctx.info.Printf("Sent expiry notification to %s", address) + app.info.Printf("Sent expiry notification to %s", address) } }() } @@ -140,25 +141,25 @@ func (ctx *appContext) checkInvite(code string, used bool, username string) bool } changed = true match = false - delete(ctx.storage.invites, code) + delete(app.storage.invites, code) } else if used { changed = true del := false newInv := inv if newInv.RemainingUses == 1 { del = true - delete(ctx.storage.invites, code) + delete(app.storage.invites, code) } else if newInv.RemainingUses != 0 { // 0 means infinite i guess? newInv.RemainingUses -= 1 } - newInv.UsedBy = append(newInv.UsedBy, []string{username, ctx.formatDatetime(current_time)}) + newInv.UsedBy = append(newInv.UsedBy, []string{username, app.formatDatetime(current_time)}) if !del { - ctx.storage.invites[code] = newInv + app.storage.invites[code] = newInv } } if changed { - ctx.storage.storeInvites() + app.storage.storeInvites() } return match } @@ -174,17 +175,17 @@ type newUserReq struct { Code string `json:"code"` } -func (ctx *appContext) NewUser(gc *gin.Context) { +func (app *appContext) NewUser(gc *gin.Context) { var req newUserReq gc.BindJSON(&req) - ctx.debug.Printf("%s: New user attempt", req.Code) - if !ctx.checkInvite(req.Code, false, "") { - ctx.info.Printf("%s New user failed: invalid code", req.Code) + app.debug.Printf("%s: New user attempt", req.Code) + if !app.checkInvite(req.Code, false, "") { + app.info.Printf("%s New user failed: invalid code", req.Code) gc.JSON(401, map[string]bool{"success": false}) gc.Abort() return } - validation := ctx.validator.validate(req.Password) + validation := app.validator.validate(req.Password) valid := true for _, val := range validation { if !val { @@ -193,38 +194,38 @@ func (ctx *appContext) NewUser(gc *gin.Context) { } if !valid { // 200 bcs idk what i did in js - ctx.info.Printf("%s New user failed: Invalid password", req.Code) + app.info.Printf("%s New user failed: Invalid password", req.Code) gc.JSON(200, validation) gc.Abort() return } - existingUser, _, _ := ctx.jf.userByName(req.Username, false) + existingUser, _, _ := app.jf.userByName(req.Username, false) if existingUser != nil { msg := fmt.Sprintf("User already exists named %s", req.Username) - ctx.info.Printf("%s New user failed: %s", req.Code, msg) + app.info.Printf("%s New user failed: %s", req.Code, msg) respond(401, msg, gc) return } - user, status, err := ctx.jf.newUser(req.Username, req.Password) + user, status, err := app.jf.newUser(req.Username, req.Password) if !(status == 200 || status == 204) || err != nil { - ctx.err.Printf("%s New user failed: Jellyfin responded with %d", req.Code, status) + app.err.Printf("%s New user failed: Jellyfin responded with %d", req.Code, status) respond(401, "Unknown error", gc) return } - ctx.checkInvite(req.Code, true, req.Username) - invite := ctx.storage.invites[req.Code] - if ctx.config.Section("notifications").Key("enabled").MustBool(false) { + app.checkInvite(req.Code, true, req.Username) + invite := app.storage.invites[req.Code] + if app.config.Section("notifications").Key("enabled").MustBool(false) { for address, settings := range invite.Notify { if settings["notify-creation"] { go func() { - if ctx.email.constructCreated(req.Code, req.Username, req.Email, invite, ctx) != nil { - ctx.err.Printf("%s: Failed to construct user creation notification", req.Code) - ctx.debug.Printf("%s: Error: %s", req.Code, err) - } else if ctx.email.send(address, ctx) != nil { - ctx.err.Printf("%s: Failed to send user creation notification", req.Code) - ctx.debug.Printf("%s: Error: %s", req.Code, err) + if app.email.constructCreated(req.Code, req.Username, req.Email, invite, app) != nil { + app.err.Printf("%s: Failed to construct user creation notification", req.Code) + app.debug.Printf("%s: Error: %s", req.Code, err) + } else if app.email.send(address, app) != nil { + app.err.Printf("%s: Failed to send user creation notification", req.Code) + app.debug.Printf("%s: Error: %s", req.Code, err) } else { - ctx.info.Printf("%s: Sent user creation notification to %s", req.Code, address) + app.info.Printf("%s: Sent user creation notification to %s", req.Code, address) } }() } @@ -234,23 +235,23 @@ func (ctx *appContext) NewUser(gc *gin.Context) { if user["Id"] != nil { id = user["Id"].(string) } - if len(ctx.storage.policy) != 0 { - status, err = ctx.jf.setPolicy(id, ctx.storage.policy) + if len(app.storage.policy) != 0 { + status, err = app.jf.setPolicy(id, app.storage.policy) if !(status == 200 || status == 204) { - ctx.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status) + app.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status) } } - if len(ctx.storage.configuration) != 0 && len(ctx.storage.displayprefs) != 0 { - status, err = ctx.jf.setConfiguration(id, ctx.storage.configuration) + if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 { + status, err = app.jf.setConfiguration(id, app.storage.configuration) if (status == 200 || status == 204) && err == nil { - status, err = ctx.jf.setDisplayPreferences(id, ctx.storage.displayprefs) + status, err = app.jf.setDisplayPreferences(id, app.storage.displayprefs) } else { - ctx.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status) + app.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status) } } - if ctx.config.Section("password_resets").Key("enabled").MustBool(false) { - ctx.storage.emails[id] = req.Email - ctx.storage.storeEmails() + if app.config.Section("password_resets").Key("enabled").MustBool(false) { + app.storage.emails[id] = req.Email + app.storage.storeEmails() } gc.JSON(200, validation) } @@ -265,10 +266,10 @@ type generateInviteReq struct { RemainingUses int `json:"remaining-uses"` } -func (ctx *appContext) GenerateInvite(gc *gin.Context) { +func (app *appContext) GenerateInvite(gc *gin.Context) { var req generateInviteReq - ctx.debug.Println("Generating new invite") - ctx.storage.loadInvites() + app.debug.Println("Generating new invite") + app.storage.loadInvites() gc.BindJSON(&req) current_time := time.Now() valid_till := current_time.AddDate(0, 0, req.Days) @@ -286,40 +287,40 @@ func (ctx *appContext) GenerateInvite(gc *gin.Context) { invite.RemainingUses = 1 } invite.ValidTill = valid_till - if req.Email != "" && ctx.config.Section("invite_emails").Key("enabled").MustBool(false) { - ctx.debug.Printf("%s: Sending invite email", invite_code) + if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { + app.debug.Printf("%s: Sending invite email", invite_code) invite.Email = req.Email - if err := ctx.email.constructInvite(invite_code, invite, ctx); err != nil { + if err := app.email.constructInvite(invite_code, invite, app); err != nil { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) - ctx.err.Printf("%s: Failed to construct invite email", invite_code) - ctx.debug.Printf("%s: Error: %s", invite_code, err) - } else if err := ctx.email.send(req.Email, ctx); err != nil { + app.err.Printf("%s: Failed to construct invite email", invite_code) + app.debug.Printf("%s: Error: %s", invite_code, err) + } else if err := app.email.send(req.Email, app); err != nil { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) - ctx.err.Printf("%s: %s", invite_code, invite.Email) - ctx.debug.Printf("%s: Error: %s", invite_code, err) + app.err.Printf("%s: %s", invite_code, invite.Email) + app.debug.Printf("%s: Error: %s", invite_code, err) } else { - ctx.info.Printf("%s: Sent invite email to %s", invite_code, req.Email) + app.info.Printf("%s: Sent invite email to %s", invite_code, req.Email) } } - ctx.storage.invites[invite_code] = invite - ctx.storage.storeInvites() + app.storage.invites[invite_code] = invite + app.storage.storeInvites() gc.JSON(200, map[string]bool{"success": true}) } -func (ctx *appContext) GetInvites(gc *gin.Context) { - ctx.debug.Println("Invites requested") +func (app *appContext) GetInvites(gc *gin.Context) { + app.debug.Println("Invites requested") current_time := time.Now() - ctx.storage.loadInvites() - ctx.checkInvites() + app.storage.loadInvites() + app.checkInvites() var invites []map[string]interface{} - for code, inv := range ctx.storage.invites { + for code, inv := range app.storage.invites { _, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, current_time) invite := make(map[string]interface{}) invite["code"] = code invite["days"] = days invite["hours"] = hours invite["minutes"] = minutes - invite["created"] = ctx.formatDatetime(inv.Created) + invite["created"] = app.formatDatetime(inv.Created) if len(inv.UsedBy) != 0 { invite["used-by"] = inv.UsedBy } @@ -335,11 +336,11 @@ func (ctx *appContext) GetInvites(gc *gin.Context) { } if len(inv.Notify) != 0 { var address string - if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) { - ctx.storage.loadEmails() - address = ctx.storage.emails[gc.GetString("jfId")].(string) + if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { + app.storage.loadEmails() + address = app.storage.emails[gc.GetString("jfId")].(string) } else { - address = ctx.config.Section("ui").Key("email").String() + address = app.config.Section("ui").Key("email").String() } if _, ok := inv.Notify[address]; ok { for _, notify_type := range []string{"notify-expiry", "notify-creation"} { @@ -362,34 +363,34 @@ type notifySetting struct { NotifyCreation bool `json:"notify-creation"` } -func (ctx *appContext) SetNotify(gc *gin.Context) { +func (app *appContext) SetNotify(gc *gin.Context) { var req map[string]notifySetting gc.BindJSON(&req) changed := false for code, settings := range req { - ctx.debug.Printf("%s: Notification settings change requested", code) - ctx.storage.loadInvites() - ctx.storage.loadEmails() - invite, ok := ctx.storage.invites[code] + app.debug.Printf("%s: Notification settings change requested", code) + app.storage.loadInvites() + app.storage.loadEmails() + invite, ok := app.storage.invites[code] if !ok { - ctx.err.Printf("%s Notification setting change failed: Invalid code", code) + app.err.Printf("%s Notification setting change failed: Invalid code", code) gc.JSON(400, map[string]string{"error": "Invalid invite code"}) gc.Abort() return } var address string - if ctx.config.Section("ui").Key("jellyfin_login").MustBool(false) { + if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { var ok bool - address, ok = ctx.storage.emails[gc.GetString("jfId")].(string) + address, ok = app.storage.emails[gc.GetString("jfId")].(string) if !ok { - ctx.err.Printf("%s: Couldn't find email address. Make sure it's set", code) - ctx.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId")) + app.err.Printf("%s: Couldn't find email address. Make sure it's set", code) + app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId")) gc.JSON(500, map[string]string{"error": "Missing user email"}) gc.Abort() return } } else { - address = ctx.config.Section("ui").Key("email").String() + address = app.config.Section("ui").Key("email").String() } if invite.Notify == nil { invite.Notify = map[string]map[string]bool{} @@ -401,20 +402,20 @@ func (ctx *appContext) SetNotify(gc *gin.Context) { */ if invite.Notify[address]["notify-expiry"] != settings.NotifyExpiry { invite.Notify[address]["notify-expiry"] = settings.NotifyExpiry - ctx.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings.NotifyExpiry, address) + app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings.NotifyExpiry, address) changed = true } if invite.Notify[address]["notify-creation"] != settings.NotifyCreation { invite.Notify[address]["notify-creation"] = settings.NotifyCreation - ctx.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings.NotifyExpiry, address) + app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings.NotifyExpiry, address) changed = true } if changed { - ctx.storage.invites[code] = invite + app.storage.invites[code] = invite } } if changed { - ctx.storage.storeInvites() + app.storage.storeInvites() } } @@ -422,20 +423,20 @@ type deleteReq struct { Code string `json:"code"` } -func (ctx *appContext) DeleteInvite(gc *gin.Context) { +func (app *appContext) DeleteInvite(gc *gin.Context) { var req deleteReq gc.BindJSON(&req) - ctx.debug.Printf("%s: Deletion requested", req.Code) + app.debug.Printf("%s: Deletion requested", req.Code) var ok bool - _, ok = ctx.storage.invites[req.Code] + _, ok = app.storage.invites[req.Code] if ok { - delete(ctx.storage.invites, req.Code) - ctx.storage.storeInvites() - ctx.info.Printf("%s: Invite deleted", req.Code) + delete(app.storage.invites, req.Code) + app.storage.storeInvites() + app.info.Printf("%s: Invite deleted", req.Code) gc.JSON(200, map[string]bool{"success": true}) return } - ctx.err.Printf("%s: Deletion failed: Invalid code", req.Code) + app.err.Printf("%s: Deletion failed: Invalid code", req.Code) respond(401, "Code doesn't exist", gc) } @@ -448,21 +449,21 @@ type respUser struct { Email string `json:"email,omitempty"` } -func (ctx *appContext) GetUsers(gc *gin.Context) { - ctx.debug.Println("Users requested") +func (app *appContext) GetUsers(gc *gin.Context) { + app.debug.Println("Users requested") var resp userResp resp.UserList = []respUser{} - users, status, err := ctx.jf.getUsers(false) + users, status, err := app.jf.getUsers(false) if !(status == 200 || status == 204) || err != nil { - ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status) - ctx.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get users from Jellyfin: Code %d", status) + app.debug.Printf("Error: %s", err) respond(500, "Couldn't get users", gc) return } for _, jfUser := range users { var user respUser user.Name = jfUser["Name"].(string) - if email, ok := ctx.storage.emails[jfUser["Id"].(string)]; ok { + if email, ok := app.storage.emails[jfUser["Id"].(string)]; ok { user.Email = email.(string) } resp.UserList = append(resp.UserList, user) @@ -470,24 +471,24 @@ func (ctx *appContext) GetUsers(gc *gin.Context) { gc.JSON(200, resp) } -func (ctx *appContext) ModifyEmails(gc *gin.Context) { +func (app *appContext) ModifyEmails(gc *gin.Context) { var req map[string]string gc.BindJSON(&req) - ctx.debug.Println("Email modification requested") - users, status, err := ctx.jf.getUsers(false) + app.debug.Println("Email modification requested") + users, status, err := app.jf.getUsers(false) if !(status == 200 || status == 204) || err != nil { - ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status) - ctx.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get users from Jellyfin: Code %d", status) + app.debug.Printf("Error: %s", err) respond(500, "Couldn't get users", gc) return } for _, jfUser := range users { if address, ok := req[jfUser["Name"].(string)]; ok { - ctx.storage.emails[jfUser["Id"].(string)] = address + app.storage.emails[jfUser["Id"].(string)] = address } } - ctx.storage.storeEmails() - ctx.info.Println("Email list modified") + app.storage.storeEmails() + app.info.Println("Email list modified") gc.JSON(200, map[string]bool{"success": true}) } @@ -496,46 +497,46 @@ type defaultsReq struct { Homescreen bool `json:"homescreen"` } -func (ctx *appContext) SetDefaults(gc *gin.Context) { +func (app *appContext) SetDefaults(gc *gin.Context) { var req defaultsReq gc.BindJSON(&req) - ctx.info.Printf("Getting user defaults from \"%s\"", req.Username) - user, status, err := ctx.jf.userByName(req.Username, false) + app.info.Printf("Getting user defaults from \"%s\"", req.Username) + user, status, err := app.jf.userByName(req.Username, false) if !(status == 200 || status == 204) || err != nil { - ctx.err.Printf("Failed to get user from Jellyfin: Code %d", status) - ctx.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get user from Jellyfin: Code %d", status) + app.debug.Printf("Error: %s", err) respond(500, "Couldn't get user", gc) return } userId := user["Id"].(string) policy := user["Policy"].(map[string]interface{}) - ctx.storage.policy = policy - ctx.storage.storePolicy() - ctx.debug.Println("User policy template stored") + app.storage.policy = policy + app.storage.storePolicy() + app.debug.Println("User policy template stored") if req.Homescreen { configuration := user["Configuration"].(map[string]interface{}) var displayprefs map[string]interface{} - displayprefs, status, err = ctx.jf.getDisplayPreferences(userId) + displayprefs, status, err = app.jf.getDisplayPreferences(userId) if !(status == 200 || status == 204) || err != nil { - ctx.err.Printf("Failed to get DisplayPrefs: Code %d", status) - ctx.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get DisplayPrefs: Code %d", status) + app.debug.Printf("Error: %s", err) respond(500, "Couldn't get displayprefs", gc) return } - ctx.storage.configuration = configuration - ctx.storage.displayprefs = displayprefs - ctx.storage.storeConfiguration() - ctx.debug.Println("Configuration template stored") - ctx.storage.storeDisplayprefs() - ctx.debug.Println("DisplayPrefs template stored") + app.storage.configuration = configuration + app.storage.displayprefs = displayprefs + app.storage.storeConfiguration() + app.debug.Println("Configuration template stored") + app.storage.storeDisplayprefs() + app.debug.Println("DisplayPrefs template stored") } gc.JSON(200, map[string]bool{"success": true}) } -func (ctx *appContext) GetConfig(gc *gin.Context) { - ctx.info.Println("Config requested") +func (app *appContext) GetConfig(gc *gin.Context) { + app.info.Println("Config requested") resp := map[string]interface{}{} - for section, settings := range ctx.configBase { + for section, settings := range app.configBase { if section == "order" { resp[section] = settings.([]interface{}) } else { @@ -547,7 +548,7 @@ func (ctx *appContext) GetConfig(gc *gin.Context) { resp[section].(map[string]interface{})[key] = values.(map[string]interface{}) if key != "meta" { dataType := resp[section].(map[string]interface{})[key].(map[string]interface{})["type"].(string) - configKey := ctx.config.Section(section).Key(key) + configKey := app.config.Section(section).Key(key) if dataType == "number" { if val, err := configKey.Int(); err == nil { resp[section].(map[string]interface{})[key].(map[string]interface{})["value"] = val @@ -565,11 +566,11 @@ func (ctx *appContext) GetConfig(gc *gin.Context) { gc.JSON(200, resp) } -func (ctx *appContext) ModifyConfig(gc *gin.Context) { - ctx.info.Println("Config modification requested") +func (app *appContext) ModifyConfig(gc *gin.Context) { + app.info.Println("Config modification requested") var req map[string]interface{} gc.BindJSON(&req) - tempConfig, _ := ini.Load(ctx.config_path) + tempConfig, _ := ini.Load(app.config_path) for section, settings := range req { _, err := tempConfig.GetSection(section) if section != "restart-program" && err == nil { @@ -578,33 +579,33 @@ func (ctx *appContext) ModifyConfig(gc *gin.Context) { } } } - tempConfig.SaveTo(ctx.config_path) - ctx.debug.Println("Config saved") + tempConfig.SaveTo(app.config_path) + app.debug.Println("Config saved") gc.JSON(200, map[string]bool{"success": true}) if req["restart-program"].(bool) { - ctx.info.Println("Restarting...") - err := ctx.Restart() + app.info.Println("Restarting...") + err := app.Restart() if err != nil { - ctx.err.Printf("Couldn't restart, try restarting manually. (%s)", err) + app.err.Printf("Couldn't restart, try restarting manually. (%s)", err) } } - ctx.loadConfig() + app.loadConfig() // Reinitialize password validator on config change, as opposed to every applicable request like in python. if _, ok := req["password_validation"]; ok { - ctx.debug.Println("Reinitializing validator") + app.debug.Println("Reinitializing validator") validatorConf := ValidatorConf{ - "characters": ctx.config.Section("password_validation").Key("min_length").MustInt(0), - "uppercase characters": ctx.config.Section("password_validation").Key("upper").MustInt(0), - "lowercase characters": ctx.config.Section("password_validation").Key("lower").MustInt(0), - "numbers": ctx.config.Section("password_validation").Key("number").MustInt(0), - "special characters": ctx.config.Section("password_validation").Key("special").MustInt(0), + "characters": app.config.Section("password_validation").Key("min_length").MustInt(0), + "uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0), + "lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0), + "numbers": app.config.Section("password_validation").Key("number").MustInt(0), + "special characters": app.config.Section("password_validation").Key("special").MustInt(0), } - if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) { + if !app.config.Section("password_validation").Key("enabled").MustBool(false) { for key := range validatorConf { validatorConf[key] = 0 } } - ctx.validator.init(validatorConf) + app.validator.init(validatorConf) } } @@ -634,11 +635,11 @@ func (ctx *appContext) ModifyConfig(gc *gin.Context) { // panic(fmt.Errorf("restarting")) // } -func (ctx *appContext) Restart() error { +func (app *appContext) Restart() error { defer func() { if r := recover(); r != nil { - signal.Notify(ctx.quit, os.Interrupt) - <-ctx.quit + signal.Notify(app.quit, os.Interrupt) + <-app.quit } }() args := os.Args diff --git a/auth.go b/auth.go index 8658abb..0527c51 100644 --- a/auth.go +++ b/auth.go @@ -3,22 +3,23 @@ package main import ( "encoding/base64" "fmt" - "github.com/dgrijalva/jwt-go" - "github.com/gin-gonic/gin" - "github.com/lithammer/shortuuid/v3" "os" "strings" "time" + + "github.com/dgrijalva/jwt-go" + "github.com/gin-gonic/gin" + "github.com/lithammer/shortuuid/v3" ) -func (ctx *appContext) webAuth() gin.HandlerFunc { - return ctx.authenticate +func (app *appContext) webAuth() gin.HandlerFunc { + return app.authenticate } -func (ctx *appContext) authenticate(gc *gin.Context) { +func (app *appContext) authenticate(gc *gin.Context) { header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) if header[0] != "Basic" { - ctx.debug.Println("Invalid authentication header") + app.debug.Println("Invalid authentication header") respond(401, "Unauthorized", gc) return } @@ -26,13 +27,13 @@ func (ctx *appContext) authenticate(gc *gin.Context) { creds := strings.SplitN(string(auth), ":", 2) token, err := jwt.Parse(creds[0], func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - ctx.debug.Printf("Invalid JWT signing method %s", token.Header["alg"]) + app.debug.Printf("Invalid JWT signing method %s", token.Header["alg"]) return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) } return []byte(os.Getenv("JFA_SECRET")), nil }) if err != nil { - ctx.debug.Printf("Auth denied: %s", err) + app.debug.Printf("Auth denied: %s", err) respond(401, "Unauthorized", gc) return } @@ -43,32 +44,32 @@ func (ctx *appContext) authenticate(gc *gin.Context) { userId = claims["id"].(string) jfId = claims["jfid"].(string) } else { - ctx.debug.Printf("Invalid token") + app.debug.Printf("Invalid token") respond(401, "Unauthorized", gc) return } match := false - for _, user := range ctx.users { + for _, user := range app.users { if user.UserID == userId { match = true } } if !match { - ctx.debug.Printf("Couldn't find user ID %s", userId) + app.debug.Printf("Couldn't find user ID %s", userId) respond(401, "Unauthorized", gc) return } gc.Set("jfId", jfId) gc.Set("userId", userId) - ctx.debug.Println("Authentication successful") + app.debug.Println("Authentication successful") gc.Next() } -func (ctx *appContext) GetToken(gc *gin.Context) { - ctx.info.Println("Token requested (login attempt)") +func (app *appContext) GetToken(gc *gin.Context) { + app.info.Println("Token requested (login attempt)") header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) if header[0] != "Basic" { - ctx.debug.Println("Invalid authentication header") + app.debug.Println("Invalid authentication header") respond(401, "Unauthorized", gc) return } @@ -76,7 +77,7 @@ func (ctx *appContext) GetToken(gc *gin.Context) { creds := strings.SplitN(string(auth), ":", 2) match := false var userId string - for _, user := range ctx.users { + for _, user := range app.users { if user.Username == creds[0] && user.Password == creds[1] { match = true userId = user.UserID @@ -84,29 +85,29 @@ func (ctx *appContext) GetToken(gc *gin.Context) { } jfId := "" if !match { - if !ctx.jellyfinLogin { - ctx.info.Println("Auth failed: Invalid username and/or password") + if !app.jellyfinLogin { + app.info.Println("Auth failed: Invalid username and/or password") respond(401, "Unauthorized", gc) return } var status int var err error var user map[string]interface{} - user, status, err = ctx.authJf.authenticate(creds[0], creds[1]) + user, status, err = app.authJf.authenticate(creds[0], creds[1]) jfId = user["Id"].(string) if status != 200 || err != nil { if status == 401 { - ctx.info.Println("Auth failed: Invalid username and/or password") + app.info.Println("Auth failed: Invalid username and/or password") respond(401, "Unauthorized", gc) return } - ctx.err.Printf("Auth failed: Couldn't authenticate with Jellyfin: Code %d", status) + app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin: Code %d", status) respond(500, "Jellyfin error", gc) return } else { - if ctx.config.Section("ui").Key("admin_only").MustBool(true) { + if app.config.Section("ui").Key("admin_only").MustBool(true) { if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) { - ctx.debug.Printf("Auth failed: User \"%s\" isn't admin", creds[0]) + app.debug.Printf("Auth failed: User \"%s\" isn't admin", creds[0]) respond(401, "Unauthorized", gc) } } @@ -114,8 +115,8 @@ func (ctx *appContext) GetToken(gc *gin.Context) { newuser.UserID = shortuuid.New() userId = newuser.UserID // uuid, nothing else identifiable! - ctx.debug.Printf("Token generated for user \"%s\"", creds[0]) - ctx.users = append(ctx.users, newuser) + app.debug.Printf("Token generated for user \"%s\"", creds[0]) + app.users = append(app.users, newuser) } } token, err := CreateToken(userId, jfId) diff --git a/config.go b/config.go index 601996b..6ed7285 100644 --- a/config.go +++ b/config.go @@ -1,9 +1,10 @@ package main import ( - "gopkg.in/ini.v1" "path/filepath" "strconv" + + "gopkg.in/ini.v1" ) /*var DeCamel ini.NameMapper = func(raw string) string { @@ -22,51 +23,51 @@ import ( return string(out) } -func (ctx *appContext) loadDefaults() (err error) { +func (app *appContext) loadDefaults() (err error) { var cfb []byte - cfb, err = ioutil.ReadFile(ctx.configBase_path) + cfb, err = ioutil.ReadFile(app.configBase_path) if err != nil { return } - json.Unmarshal(cfb, ctx.defaults) + json.Unmarshal(cfb, app.defaults) return }*/ -func (ctx *appContext) loadConfig() error { +func (app *appContext) loadConfig() error { var err error - ctx.config, err = ini.Load(ctx.config_path) + app.config, err = ini.Load(app.config_path) if err != nil { return err } - ctx.config.Section("jellyfin").Key("public_server").SetValue(ctx.config.Section("jellyfin").Key("public_server").MustString(ctx.config.Section("jellyfin").Key("server").String())) + app.config.Section("jellyfin").Key("public_server").SetValue(app.config.Section("jellyfin").Key("public_server").MustString(app.config.Section("jellyfin").Key("server").String())) - for _, key := range ctx.config.Section("files").Keys() { + for _, key := range app.config.Section("files").Keys() { // if key.MustString("") == "" && key.Name() != "custom_css" { - // key.SetValue(filepath.Join(ctx.data_path, (key.Name() + ".json"))) + // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json"))) // } - key.SetValue(key.MustString(filepath.Join(ctx.data_path, (key.Name() + ".json")))) + key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json")))) } for _, key := range []string{"user_configuration", "user_displayprefs"} { - // if ctx.config.Section("files").Key(key).MustString("") == "" { - // key.SetValue(filepath.Join(ctx.data_path, (key.Name() + ".json"))) + // if app.config.Section("files").Key(key).MustString("") == "" { + // key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json"))) // } - ctx.config.Section("files").Key(key).SetValue(ctx.config.Section("files").Key(key).MustString(filepath.Join(ctx.data_path, (key + ".json")))) + app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.data_path, (key + ".json")))) } - ctx.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(ctx.config.Section("email").Key("no_username").MustBool(false))) + app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false))) - ctx.config.Section("password_resets").Key("email_html").SetValue(ctx.config.Section("password_resets").Key("email_html").MustString(filepath.Join(ctx.local_path, "email.html"))) - ctx.config.Section("password_resets").Key("email_text").SetValue(ctx.config.Section("password_resets").Key("email_text").MustString(filepath.Join(ctx.local_path, "email.txt"))) + app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString(filepath.Join(app.local_path, "email.html"))) + app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString(filepath.Join(app.local_path, "email.txt"))) - ctx.config.Section("invite_emails").Key("email_html").SetValue(ctx.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(ctx.local_path, "invite-email.html"))) - ctx.config.Section("invite_emails").Key("email_text").SetValue(ctx.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(ctx.local_path, "invite-email.txt"))) + app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(app.local_path, "invite-email.html"))) + app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(app.local_path, "invite-email.txt"))) - ctx.config.Section("notifications").Key("expiry_html").SetValue(ctx.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(ctx.local_path, "expired.html"))) - ctx.config.Section("notifications").Key("expiry_text").SetValue(ctx.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(ctx.local_path, "expired.txt"))) + app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(app.local_path, "expired.html"))) + app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(app.local_path, "expired.txt"))) - ctx.config.Section("notifications").Key("created_html").SetValue(ctx.config.Section("notifications").Key("created_html").MustString(filepath.Join(ctx.local_path, "created.html"))) - ctx.config.Section("notifications").Key("created_text").SetValue(ctx.config.Section("notifications").Key("created_text").MustString(filepath.Join(ctx.local_path, "created.txt"))) + app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.local_path, "created.html"))) + app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.local_path, "created.txt"))) return nil } diff --git a/daemon.go b/daemon.go index c194bd6..a8827e1 100644 --- a/daemon.go +++ b/daemon.go @@ -9,21 +9,21 @@ type Repeater struct { ShutdownChannel chan string Interval time.Duration period time.Duration - ctx *appContext + app *appContext } -func NewRepeater(interval time.Duration, ctx *appContext) *Repeater { +func NewRepeater(interval time.Duration, app *appContext) *Repeater { return &Repeater{ Stopped: false, ShutdownChannel: make(chan string), Interval: interval, period: interval, - ctx: ctx, + app: app, } } func (rt *Repeater) Run() { - rt.ctx.info.Println("Invite daemon started") + rt.app.info.Println("Invite daemon started") for { select { case <-rt.ShutdownChannel: @@ -33,9 +33,9 @@ func (rt *Repeater) Run() { break } started := time.Now() - rt.ctx.storage.loadInvites() - rt.ctx.debug.Println("Daemon: Checking invites") - rt.ctx.checkInvites() + rt.app.storage.loadInvites() + rt.app.debug.Println("Daemon: Checking invites") + rt.app.checkInvites() finished := time.Now() duration := finished.Sub(started) rt.period = rt.Interval - duration diff --git a/email.go b/email.go index b8eafa6..5baa6a8 100644 --- a/email.go +++ b/email.go @@ -5,13 +5,14 @@ import ( "context" "crypto/tls" "fmt" - jEmail "github.com/jordan-wright/email" - "github.com/knz/strtime" - "github.com/mailgun/mailgun-go/v4" "html/template" "net/smtp" "strings" "time" + + jEmail "github.com/jordan-wright/email" + "github.com/knz/strtime" + "github.com/mailgun/mailgun-go/v4" ) type Emailer struct { @@ -49,13 +50,13 @@ func (email *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern, return } -func (email *Emailer) init(ctx *appContext) { - email.fromAddr = ctx.config.Section("email").Key("address").String() - email.fromName = ctx.config.Section("email").Key("from").String() - email.sendMethod = ctx.config.Section("email").Key("method").String() +func (email *Emailer) init(app *appContext) { + email.fromAddr = app.config.Section("email").Key("address").String() + email.fromName = app.config.Section("email").Key("from").String() + email.sendMethod = app.config.Section("email").Key("method").String() if email.sendMethod == "mailgun" { - email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], ctx.config.Section("mailgun").Key("api_key").String()) - api_url := ctx.config.Section("mailgun").Key("api_url").String() + email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], app.config.Section("mailgun").Key("api_key").String()) + api_url := app.config.Section("mailgun").Key("api_url").String() // Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages' if strings.Contains(api_url, "messages") { api_url = api_url[0:strings.LastIndex(api_url, "/")] @@ -63,21 +64,21 @@ func (email *Emailer) init(ctx *appContext) { } email.mg.SetAPIBase(api_url) } else if email.sendMethod == "smtp" { - ctx.host = ctx.config.Section("smtp").Key("server").String() - email.smtpAuth = smtp.PlainAuth("", email.fromAddr, ctx.config.Section("smtp").Key("password").String(), ctx.host) + app.host = app.config.Section("smtp").Key("server").String() + email.smtpAuth = smtp.PlainAuth("", email.fromAddr, app.config.Section("smtp").Key("password").String(), app.host) } } -func (email *Emailer) constructInvite(code string, invite Invite, ctx *appContext) error { - email.content.subject = ctx.config.Section("invite_emails").Key("subject").String() +func (email *Emailer) constructInvite(code string, invite Invite, app *appContext) error { + email.content.subject = app.config.Section("invite_emails").Key("subject").String() expiry := invite.ValidTill - d, t, expires_in := email.formatExpiry(expiry, false, ctx.datePattern, ctx.timePattern) - message := ctx.config.Section("email").Key("message").String() - invite_link := ctx.config.Section("invite_emails").Key("url_base").String() + d, t, expires_in := email.formatExpiry(expiry, false, app.datePattern, app.timePattern) + message := app.config.Section("email").Key("message").String() + invite_link := app.config.Section("invite_emails").Key("url_base").String() invite_link = fmt.Sprintf("%s/%s", invite_link, code) for _, key := range []string{"html", "text"} { - fpath := ctx.config.Section("invite_emails").Key("email_" + key).String() + fpath := app.config.Section("invite_emails").Key("email_" + key).String() tpl, err := template.ParseFiles(fpath) if err != nil { return err @@ -103,11 +104,11 @@ func (email *Emailer) constructInvite(code string, invite Invite, ctx *appContex return nil } -func (email *Emailer) constructExpiry(code string, invite Invite, ctx *appContext) error { +func (email *Emailer) constructExpiry(code string, invite Invite, app *appContext) error { email.content.subject = "Notice: Invite expired" - expiry := ctx.formatDatetime(invite.ValidTill) + expiry := app.formatDatetime(invite.ValidTill) for _, key := range []string{"html", "text"} { - fpath := ctx.config.Section("notifications").Key("expiry_" + key).String() + fpath := app.config.Section("notifications").Key("expiry_" + key).String() tpl, err := template.ParseFiles(fpath) if err != nil { return err @@ -130,17 +131,17 @@ func (email *Emailer) constructExpiry(code string, invite Invite, ctx *appContex return nil } -func (email *Emailer) constructCreated(code, username, address string, invite Invite, ctx *appContext) error { +func (email *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) error { email.content.subject = "Notice: User created" - created := ctx.formatDatetime(invite.Created) + created := app.formatDatetime(invite.Created) var tplAddress string - if ctx.config.Section("email").Key("no_username").MustBool(false) { + if app.config.Section("email").Key("no_username").MustBool(false) { tplAddress = "n/a" } else { tplAddress = address } for _, key := range []string{"html", "text"} { - fpath := ctx.config.Section("notifications").Key("created_" + key).String() + fpath := app.config.Section("notifications").Key("created_" + key).String() tpl, err := template.ParseFiles(fpath) if err != nil { return err @@ -165,12 +166,12 @@ func (email *Emailer) constructCreated(code, username, address string, invite In return nil } -func (email *Emailer) constructReset(pwr Pwr, ctx *appContext) error { - email.content.subject = ctx.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin") - d, t, expires_in := email.formatExpiry(pwr.Expiry, true, ctx.datePattern, ctx.timePattern) - message := ctx.config.Section("email").Key("message").String() +func (email *Emailer) constructReset(pwr Pwr, app *appContext) error { + email.content.subject = app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin") + d, t, expires_in := email.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern) + message := app.config.Section("email").Key("message").String() for _, key := range []string{"html", "text"} { - fpath := ctx.config.Section("password_resets").Key("email_" + key).String() + fpath := app.config.Section("password_resets").Key("email_" + key).String() tpl, err := template.ParseFiles(fpath) if err != nil { return err @@ -197,7 +198,7 @@ func (email *Emailer) constructReset(pwr Pwr, ctx *appContext) error { return nil } -func (email *Emailer) send(address string, ctx *appContext) error { +func (email *Emailer) send(address string, app *appContext) error { if email.sendMethod == "mailgun" { message := email.mg.NewMessage( fmt.Sprintf("%s <%s>", email.fromName, email.fromAddr), @@ -205,9 +206,9 @@ func (email *Emailer) send(address string, ctx *appContext) error { email.content.text, address) message.SetHtml(email.content.html) - mgctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + mgapp, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() - _, _, err := email.mg.Send(mgctx, message) + _, _, err := email.mg.Send(mgapp, message) if err != nil { return err } @@ -218,19 +219,19 @@ func (email *Emailer) send(address string, ctx *appContext) error { e.To = []string{address} e.Text = []byte(email.content.text) e.HTML = []byte(email.content.html) - smtpType := ctx.config.Section("smtp").Key("encryption").String() + smtpType := app.config.Section("smtp").Key("encryption").String() tlsConfig := &tls.Config{ InsecureSkipVerify: false, - ServerName: ctx.host, + ServerName: app.host, } var err error if smtpType == "ssl_tls" { - port := ctx.config.Section("smtp").Key("port").MustInt(465) - server := fmt.Sprintf("%s:%d", ctx.host, port) + port := app.config.Section("smtp").Key("port").MustInt(465) + server := fmt.Sprintf("%s:%d", app.host, port) err = e.SendWithTLS(server, email.smtpAuth, tlsConfig) } else if smtpType == "starttls" { - port := ctx.config.Section("smtp").Key("port").MustInt(587) - server := fmt.Sprintf("%s:%d", ctx.host, port) + port := app.config.Section("smtp").Key("port").MustInt(587) + server := fmt.Sprintf("%s:%d", app.host, port) e.SendWithStartTLS(server, email.smtpAuth, tlsConfig) } return err diff --git a/main.go b/main.go index ec50a22..b9af73c 100644 --- a/main.go +++ b/main.go @@ -7,11 +7,6 @@ import ( "encoding/json" "flag" "fmt" - "github.com/gin-contrib/pprof" - "github.com/gin-contrib/static" - "github.com/gin-gonic/gin" - "github.com/lithammer/shortuuid/v3" - "gopkg.in/ini.v1" "io" "io/ioutil" "log" @@ -20,6 +15,12 @@ import ( "os/signal" "path/filepath" "time" + + "github.com/gin-contrib/pprof" + "github.com/gin-contrib/static" + "github.com/gin-gonic/gin" + "github.com/lithammer/shortuuid/v3" + "gopkg.in/ini.v1" ) // Username is JWT! @@ -95,190 +96,190 @@ func setGinLogger(router *gin.Engine, debugMode bool) { } func main() { - ctx := new(appContext) + app := new(appContext) userConfigDir, _ := os.UserConfigDir() - ctx.data_path = filepath.Join(userConfigDir, "jfa-go") - ctx.config_path = filepath.Join(ctx.data_path, "config.ini") + app.data_path = filepath.Join(userConfigDir, "jfa-go") + app.config_path = filepath.Join(app.data_path, "config.ini") executable, _ := os.Executable() - ctx.local_path = filepath.Join(filepath.Dir(executable), "data") + app.local_path = filepath.Join(filepath.Dir(executable), "data") - ctx.info = log.New(os.Stdout, "[INFO] ", log.Ltime) - ctx.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile) + app.info = log.New(os.Stdout, "[INFO] ", log.Ltime) + app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile) - dataPath := flag.String("data", ctx.data_path, "alternate path to data directory.") - configPath := flag.String("config", ctx.config_path, "alternate path to config file.") + dataPath := flag.String("data", app.data_path, "alternate path to data directory.") + configPath := flag.String("config", app.config_path, "alternate path to config file.") host := flag.String("host", "", "alternate address to host web ui on.") port := flag.Int("port", 0, "alternate port to host web ui on.") flag.Parse() - if ctx.config_path == *configPath && ctx.data_path != *dataPath { - ctx.config_path = filepath.Join(*dataPath, "config.ini") + if app.config_path == *configPath && app.data_path != *dataPath { + app.config_path = filepath.Join(*dataPath, "config.ini") } else { - ctx.config_path = *configPath - ctx.data_path = *dataPath + app.config_path = *configPath + app.data_path = *dataPath } // Env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason. if v := os.Getenv("JFA_CONFIGPATH"); v != "" { - ctx.config_path = v + app.config_path = v } if v := os.Getenv("JFA_DATAPATH"); v != "" { - ctx.data_path = v + app.data_path = v } - os.Setenv("JFA_CONFIGPATH", ctx.config_path) - os.Setenv("JFA_DATAPATH", ctx.data_path) + os.Setenv("JFA_CONFIGPATH", app.config_path) + os.Setenv("JFA_DATAPATH", app.data_path) var firstRun bool - if _, err := os.Stat(ctx.data_path); os.IsNotExist(err) { - os.Mkdir(ctx.data_path, 0700) + if _, err := os.Stat(app.data_path); os.IsNotExist(err) { + os.Mkdir(app.data_path, 0700) } - if _, err := os.Stat(ctx.config_path); os.IsNotExist(err) { + if _, err := os.Stat(app.config_path); os.IsNotExist(err) { firstRun = true - dConfigPath := filepath.Join(ctx.local_path, "config-default.ini") + dConfigPath := filepath.Join(app.local_path, "config-default.ini") var dConfig *os.File dConfig, err = os.Open(dConfigPath) if err != nil { - ctx.err.Fatalf("Couldn't find default config file \"%s\"", dConfigPath) + app.err.Fatalf("Couldn't find default config file \"%s\"", dConfigPath) } defer dConfig.Close() var nConfig *os.File - nConfig, err := os.Create(ctx.config_path) + nConfig, err := os.Create(app.config_path) if err != nil { - ctx.err.Fatalf("Couldn't open config file for writing: \"%s\"", dConfigPath) + app.err.Fatalf("Couldn't open config file for writing: \"%s\"", dConfigPath) } defer nConfig.Close() _, err = io.Copy(nConfig, dConfig) if err != nil { - ctx.err.Fatalf("Couldn't copy default config. To do this manually, copy\n%s\nto\n%s", dConfigPath, ctx.config_path) + app.err.Fatalf("Couldn't copy default config. To do this manually, copy\n%s\nto\n%s", dConfigPath, app.config_path) } - ctx.info.Printf("Copied default configuration to \"%s\"", ctx.config_path) + app.info.Printf("Copied default configuration to \"%s\"", app.config_path) } var debugMode bool var address string - if ctx.loadConfig() != nil { - ctx.err.Fatalf("Failed to load config file \"%s\"", ctx.config_path) + if app.loadConfig() != nil { + app.err.Fatalf("Failed to load config file \"%s\"", app.config_path) } - ctx.version = ctx.config.Section("jellyfin").Key("version").String() + app.version = app.config.Section("jellyfin").Key("version").String() - debugMode = ctx.config.Section("ui").Key("debug").MustBool(true) + debugMode = app.config.Section("ui").Key("debug").MustBool(true) if debugMode { - ctx.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile) + app.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile) } else { - ctx.debug = log.New(ioutil.Discard, "", 0) + app.debug = log.New(ioutil.Discard, "", 0) } if !firstRun { - ctx.host = ctx.config.Section("ui").Key("host").String() - ctx.port = ctx.config.Section("ui").Key("port").MustInt(8056) + app.host = app.config.Section("ui").Key("host").String() + app.port = app.config.Section("ui").Key("port").MustInt(8056) - if *host != ctx.host && *host != "" { - ctx.host = *host + if *host != app.host && *host != "" { + app.host = *host } - if *port != ctx.port && *port > 0 { - ctx.port = *port + if *port != app.port && *port > 0 { + app.port = *port } if h := os.Getenv("JFA_HOST"); h != "" { - ctx.host = h + app.host = h if p := os.Getenv("JFA_PORT"); p != "" { var port int _, err := fmt.Sscan(p, &port) if err == nil { - ctx.port = port + app.port = port } } } - address = fmt.Sprintf("%s:%d", ctx.host, ctx.port) + address = fmt.Sprintf("%s:%d", app.host, app.port) - ctx.debug.Printf("Loaded config file \"%s\"", ctx.config_path) + app.debug.Printf("Loaded config file \"%s\"", app.config_path) - if ctx.config.Section("ui").Key("bs5").MustBool(false) { - ctx.cssFile = "bs5-jf.css" - ctx.bsVersion = 5 + if app.config.Section("ui").Key("bs5").MustBool(false) { + app.cssFile = "bs5-jf.css" + app.bsVersion = 5 } else { - ctx.cssFile = "bs4-jf.css" - ctx.bsVersion = 4 + app.cssFile = "bs4-jf.css" + app.bsVersion = 4 } - ctx.debug.Println("Loading storage") + app.debug.Println("Loading storage") - ctx.storage.invite_path = filepath.Join(ctx.data_path, "invites.json") - ctx.storage.loadInvites() - ctx.storage.emails_path = filepath.Join(ctx.data_path, "emails.json") - ctx.storage.loadEmails() - ctx.storage.policy_path = filepath.Join(ctx.data_path, "user_template.json") - ctx.storage.loadPolicy() - ctx.storage.configuration_path = filepath.Join(ctx.data_path, "user_configuration.json") - ctx.storage.loadConfiguration() - ctx.storage.displayprefs_path = filepath.Join(ctx.data_path, "user_displayprefs.json") - ctx.storage.loadDisplayprefs() + app.storage.invite_path = filepath.Join(app.data_path, "invites.json") + app.storage.loadInvites() + app.storage.emails_path = filepath.Join(app.data_path, "emails.json") + app.storage.loadEmails() + app.storage.policy_path = filepath.Join(app.data_path, "user_template.json") + app.storage.loadPolicy() + app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json") + app.storage.loadConfiguration() + app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json") + app.storage.loadDisplayprefs() - ctx.configBase_path = filepath.Join(ctx.local_path, "config-base.json") - config_base, _ := ioutil.ReadFile(ctx.configBase_path) - json.Unmarshal(config_base, &ctx.configBase) + app.configBase_path = filepath.Join(app.local_path, "config-base.json") + config_base, _ := ioutil.ReadFile(app.configBase_path) + json.Unmarshal(config_base, &app.configBase) themes := map[string]string{ - "Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", ctx.bsVersion), - "Bootstrap (Light)": fmt.Sprintf("bs%d.css", ctx.bsVersion), + "Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", app.bsVersion), + "Bootstrap (Light)": fmt.Sprintf("bs%d.css", app.bsVersion), "Custom CSS": "", } - if val, ok := themes[ctx.config.Section("ui").Key("theme").String()]; ok { - ctx.cssFile = val + if val, ok := themes[app.config.Section("ui").Key("theme").String()]; ok { + app.cssFile = val } - ctx.debug.Printf("Using css file \"%s\"", ctx.cssFile) + app.debug.Printf("Using css file \"%s\"", app.cssFile) secret, err := GenerateSecret(16) if err != nil { - ctx.err.Fatal(err) + app.err.Fatal(err) } os.Setenv("JFA_SECRET", secret) - ctx.jellyfinLogin = true - if val, _ := ctx.config.Section("ui").Key("jellyfin_login").Bool(); !val { - ctx.jellyfinLogin = false + app.jellyfinLogin = true + if val, _ := app.config.Section("ui").Key("jellyfin_login").Bool(); !val { + app.jellyfinLogin = false user := User{} user.UserID = shortuuid.New() - user.Username = ctx.config.Section("ui").Key("username").String() - user.Password = ctx.config.Section("ui").Key("password").String() - ctx.users = append(ctx.users, user) + user.Username = app.config.Section("ui").Key("username").String() + user.Password = app.config.Section("ui").Key("password").String() + app.users = append(app.users, user) } else { - ctx.debug.Println("Using Jellyfin for authentication") + app.debug.Println("Using Jellyfin for authentication") } - server := ctx.config.Section("jellyfin").Key("server").String() - ctx.jf.init(server, "jfa-go", ctx.version, "hrfee-arch", "hrfee-arch") + server := app.config.Section("jellyfin").Key("server").String() + app.jf.init(server, "jfa-go", app.version, "hrfee-arch", "hrfee-arch") var status int - _, status, err = ctx.jf.authenticate(ctx.config.Section("jellyfin").Key("username").String(), ctx.config.Section("jellyfin").Key("password").String()) + _, status, err = app.jf.authenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String()) if status != 200 || err != nil { - ctx.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status) + app.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status) } - ctx.info.Printf("Authenticated with %s", server) - ctx.authJf.init(server, "jfa-go", ctx.version, "auth", "auth") + app.info.Printf("Authenticated with %s", server) + app.authJf.init(server, "jfa-go", app.version, "auth", "auth") - ctx.loadStrftime() + app.loadStrftime() validatorConf := ValidatorConf{ - "characters": ctx.config.Section("password_validation").Key("min_length").MustInt(0), - "uppercase characters": ctx.config.Section("password_validation").Key("upper").MustInt(0), - "lowercase characters": ctx.config.Section("password_validation").Key("lower").MustInt(0), - "numbers": ctx.config.Section("password_validation").Key("number").MustInt(0), - "special characters": ctx.config.Section("password_validation").Key("special").MustInt(0), + "characters": app.config.Section("password_validation").Key("min_length").MustInt(0), + "uppercase characters": app.config.Section("password_validation").Key("upper").MustInt(0), + "lowercase characters": app.config.Section("password_validation").Key("lower").MustInt(0), + "numbers": app.config.Section("password_validation").Key("number").MustInt(0), + "special characters": app.config.Section("password_validation").Key("special").MustInt(0), } - if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) { + if !app.config.Section("password_validation").Key("enabled").MustBool(false) { for key := range validatorConf { validatorConf[key] = 0 } } - ctx.validator.init(validatorConf) + app.validator.init(validatorConf) - ctx.email.init(ctx) + app.email.init(app) - inviteDaemon := NewRepeater(time.Duration(60*time.Second), ctx) + inviteDaemon := NewRepeater(time.Duration(60*time.Second), app) go inviteDaemon.Run() - if ctx.config.Section("password_resets").Key("enabled").MustBool(false) { - go ctx.StartPWR() + if app.config.Section("password_resets").Key("enabled").MustBool(false) { + go app.StartPWR() } } else { debugMode = false @@ -286,43 +287,43 @@ func main() { address = "0.0.0.0:8056" } - ctx.info.Println("Loading routes") + app.info.Println("Loading routes") router := gin.New() setGinLogger(router, debugMode) router.Use(gin.Recovery()) - router.Use(static.Serve("/", static.LocalFile(filepath.Join(ctx.local_path, "static"), false))) - router.LoadHTMLGlob(filepath.Join(ctx.local_path, "templates", "*")) - router.NoRoute(ctx.NoRouteHandler) + router.Use(static.Serve("/", static.LocalFile(filepath.Join(app.local_path, "static"), false))) + router.LoadHTMLGlob(filepath.Join(app.local_path, "templates", "*")) + router.NoRoute(app.NoRouteHandler) if debugMode { - ctx.debug.Println("Loading pprof") + app.debug.Println("Loading pprof") pprof.Register(router) } if !firstRun { - router.GET("/", ctx.AdminPage) - router.GET("/getToken", ctx.GetToken) - router.POST("/newUser", ctx.NewUser) - router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(ctx.local_path, "static"), false))) - router.GET("/invite/:invCode", ctx.InviteProxy) - api := router.Group("/", ctx.webAuth()) - api.POST("/generateInvite", ctx.GenerateInvite) - api.GET("/getInvites", ctx.GetInvites) - api.POST("/setNotify", ctx.SetNotify) - api.POST("/deleteInvite", ctx.DeleteInvite) - api.GET("/getUsers", ctx.GetUsers) - api.POST("/modifyUsers", ctx.ModifyEmails) - api.POST("/setDefaults", ctx.SetDefaults) - api.GET("/getConfig", ctx.GetConfig) - api.POST("/modifyConfig", ctx.ModifyConfig) - ctx.info.Printf("Starting router @ %s", address) + router.GET("/", app.AdminPage) + router.GET("/getToken", app.GetToken) + router.POST("/newUser", app.NewUser) + router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.local_path, "static"), false))) + router.GET("/invite/:invCode", app.InviteProxy) + api := router.Group("/", app.webAuth()) + api.POST("/generateInvite", app.GenerateInvite) + api.GET("/getInvites", app.GetInvites) + api.POST("/setNotify", app.SetNotify) + api.POST("/deleteInvite", app.DeleteInvite) + api.GET("/getUsers", app.GetUsers) + api.POST("/modifyUsers", app.ModifyEmails) + api.POST("/setDefaults", app.SetDefaults) + api.GET("/getConfig", app.GetConfig) + api.POST("/modifyConfig", app.ModifyConfig) + app.info.Printf("Starting router @ %s", address) } else { router.GET("/", func(gc *gin.Context) { gc.HTML(200, "setup.html", gin.H{}) }) - router.POST("/testJF", ctx.TestJF) - router.POST("/modifyConfig", ctx.ModifyConfig) - ctx.info.Printf("Loading setup @ %s", address) + router.POST("/testJF", app.TestJF) + router.POST("/modifyConfig", app.ModifyConfig) + app.info.Printf("Loading setup @ %s", address) } srv := &http.Server{ @@ -331,17 +332,17 @@ func main() { } go func() { if err := srv.ListenAndServe(); err != nil { - ctx.err.Printf("Failure serving: %s", err) + app.err.Printf("Failure serving: %s", err) } }() - ctx.quit = make(chan os.Signal) - signal.Notify(ctx.quit, os.Interrupt) - <-ctx.quit - ctx.info.Println("Shutting down...") + app.quit = make(chan os.Signal) + signal.Notify(app.quit, os.Interrupt) + <-app.quit + app.info.Println("Shutting down...") cntx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() if err := srv.Shutdown(cntx); err != nil { - ctx.err.Fatalf("Server shutdown error: %s", err) + app.err.Fatalf("Server shutdown error: %s", err) } } diff --git a/pwreset.go b/pwreset.go index 3b792dc..5455f08 100644 --- a/pwreset.go +++ b/pwreset.go @@ -2,33 +2,34 @@ package main import ( "encoding/json" - "github.com/fsnotify/fsnotify" "io/ioutil" "os" "strings" "time" + + "github.com/fsnotify/fsnotify" ) -func (ctx *appContext) StartPWR() { - ctx.info.Println("Starting password reset daemon") - path := ctx.config.Section("password_resets").Key("watch_directory").String() +func (app *appContext) StartPWR() { + app.info.Println("Starting password reset daemon") + path := app.config.Section("password_resets").Key("watch_directory").String() if _, err := os.Stat(path); os.IsNotExist(err) { - ctx.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path) + app.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path) return } watcher, err := fsnotify.NewWatcher() if err != nil { - ctx.err.Printf("Couldn't initialise password reset daemon") + app.err.Printf("Couldn't initialise password reset daemon") return } defer watcher.Close() done := make(chan bool) - go pwrMonitor(ctx, watcher) + go pwrMonitor(app, watcher) err = watcher.Add(path) if err != nil { - ctx.err.Printf("Failed to start password reset daemon: %s", err) + app.err.Printf("Failed to start password reset daemon: %s", err) } <-done } @@ -39,7 +40,7 @@ type Pwr struct { Expiry time.Time `json:"ExpirationDate"` } -func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) { +func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) { for { select { case event, ok := <-watcher.Events: @@ -56,29 +57,29 @@ func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) { if len(pwr.Pin) == 0 || err != nil { return } - ctx.info.Printf("New password reset for user \"%s\"", pwr.Username) + app.info.Printf("New password reset for user \"%s\"", pwr.Username) if ct := time.Now(); pwr.Expiry.After(ct) { - user, status, err := ctx.jf.userByName(pwr.Username, false) + user, status, err := app.jf.userByName(pwr.Username, false) if !(status == 200 || status == 204) || err != nil { - ctx.err.Printf("Failed to get users from Jellyfin: Code %d", status) - ctx.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get users from Jellyfin: Code %d", status) + app.debug.Printf("Error: %s", err) return } - ctx.storage.loadEmails() - address, ok := ctx.storage.emails[user["Id"].(string)].(string) + app.storage.loadEmails() + address, ok := app.storage.emails[user["Id"].(string)].(string) if !ok { - ctx.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username) + app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username) return } - if ctx.email.constructReset(pwr, ctx) != nil { - ctx.err.Printf("Failed to construct password reset email for %s", pwr.Username) - } else if ctx.email.send(address, ctx) != nil { - ctx.err.Printf("Failed to send password reset email to \"%s\"", address) + if app.email.constructReset(pwr, app) != nil { + app.err.Printf("Failed to construct password reset email for %s", pwr.Username) + } else if app.email.send(address, app) != nil { + app.err.Printf("Failed to send password reset email to \"%s\"", address) } else { - ctx.info.Printf("Sent password reset email to \"%s\"", address) + app.info.Printf("Sent password reset email to \"%s\"", address) } } else { - ctx.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry) + app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry) } } @@ -86,7 +87,7 @@ func pwrMonitor(ctx *appContext, watcher *fsnotify.Watcher) { if !ok { return } - ctx.err.Printf("Password reset daemon: %s", err) + app.err.Printf("Password reset daemon: %s", err) } } } diff --git a/setup.go b/setup.go index 2fc0dcd..bf246ce 100644 --- a/setup.go +++ b/setup.go @@ -10,14 +10,14 @@ type testReq struct { Password string `json:"jfPassword"` } -func (ctx *appContext) TestJF(gc *gin.Context) { +func (app *appContext) TestJF(gc *gin.Context) { var req testReq gc.BindJSON(&req) tempjf := Jellyfin{} - tempjf.init(req.Host, "jfa-go-setup", ctx.version, "auth", "auth") + tempjf.init(req.Host, "jfa-go-setup", app.version, "auth", "auth") _, status, err := tempjf.authenticate(req.Username, req.Password) if !(status == 200 || status == 204) || err != nil { - ctx.info.Printf("Auth failed with code %d (%s)", status, err) + app.info.Printf("Auth failed with code %d (%s)", status, err) gc.JSON(401, map[string]bool{"success": false}) return } diff --git a/views.go b/views.go index ff06151..41611a2 100644 --- a/views.go +++ b/views.go @@ -1,54 +1,55 @@ package main import ( - "github.com/gin-gonic/gin" "net/http" + + "github.com/gin-gonic/gin" ) -func (ctx *appContext) AdminPage(gc *gin.Context) { - bs5 := ctx.config.Section("ui").Key("bs5").MustBool(false) - emailEnabled, _ := ctx.config.Section("invite_emails").Key("enabled").Bool() - notificationsEnabled, _ := ctx.config.Section("notifications").Key("enabled").Bool() +func (app *appContext) AdminPage(gc *gin.Context) { + bs5 := app.config.Section("ui").Key("bs5").MustBool(false) + emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() + notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() gc.HTML(http.StatusOK, "admin.html", gin.H{ "bs5": bs5, - "cssFile": ctx.cssFile, + "cssFile": app.cssFile, "contactMessage": "", "email_enabled": emailEnabled, "notifications": notificationsEnabled, }) } -func (ctx *appContext) InviteProxy(gc *gin.Context) { +func (app *appContext) InviteProxy(gc *gin.Context) { code := gc.Param("invCode") /* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */ - // if ctx.checkInvite(code, false, "") { - if _, ok := ctx.storage.invites[code]; ok { - email := ctx.storage.invites[code].Email + // if app.checkInvite(code, false, "") { + if _, ok := app.storage.invites[code]; ok { + email := app.storage.invites[code].Email gc.HTML(http.StatusOK, "form.html", gin.H{ - "bs5": ctx.config.Section("ui").Key("bs5").MustBool(false), - "cssFile": ctx.cssFile, - "contactMessage": ctx.config.Section("ui").Key("contac_message").String(), - "helpMessage": ctx.config.Section("ui").Key("help_message").String(), - "successMessage": ctx.config.Section("ui").Key("success_message").String(), - "jfLink": ctx.config.Section("jellyfin").Key("public_server").String(), - "validate": ctx.config.Section("password_validation").Key("enabled").MustBool(false), - "requirements": ctx.validator.getCriteria(), + "bs5": app.config.Section("ui").Key("bs5").MustBool(false), + "cssFile": app.cssFile, + "contactMessage": app.config.Section("ui").Key("contac_message").String(), + "helpMessage": app.config.Section("ui").Key("help_message").String(), + "successMessage": app.config.Section("ui").Key("success_message").String(), + "jfLink": app.config.Section("jellyfin").Key("public_server").String(), + "validate": app.config.Section("password_validation").Key("enabled").MustBool(false), + "requirements": app.validator.getCriteria(), "email": email, - "username": !ctx.config.Section("email").Key("no_username").MustBool(false), + "username": !app.config.Section("email").Key("no_username").MustBool(false), }) } else { gc.HTML(404, "invalidCode.html", gin.H{ - "bs5": ctx.config.Section("ui").Key("bs5").MustBool(false), - "cssFile": ctx.cssFile, - "contactMessage": ctx.config.Section("ui").Key("contac_message").String(), + "bs5": app.config.Section("ui").Key("bs5").MustBool(false), + "cssFile": app.cssFile, + "contactMessage": app.config.Section("ui").Key("contac_message").String(), }) } } -func (ctx *appContext) NoRouteHandler(gc *gin.Context) { +func (app *appContext) NoRouteHandler(gc *gin.Context) { gc.HTML(404, "404.html", gin.H{ - "bs5": ctx.config.Section("ui").Key("bs5").MustBool(false), - "cssFile": ctx.cssFile, - "contactMessage": ctx.config.Section("ui").Key("contact_message").String(), + "bs5": app.config.Section("ui").Key("bs5").MustBool(false), + "cssFile": app.cssFile, + "contactMessage": app.config.Section("ui").Key("contact_message").String(), }) }