From 456ef556b1c086c237831fcfa60225428665903c Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 31 Jan 2021 18:50:04 +0000 Subject: [PATCH] add inter-section dependency for settings Currently used to hide all email sections when [email]/method is blank (disabled). --- api.go | 69 ++++++++--------------------------- config.go | 8 ++++ config/config-base.json | 49 +++++++++++++++---------- lang.go | 44 ++++++++++------------ models.go | 8 ++-- pwreset.go | 3 ++ ts/modules/settings.ts | 81 ++++++++++++++++++++++++++++------------- 7 files changed, 136 insertions(+), 126 deletions(-) diff --git a/api.go b/api.go index 65aaab5..0f6982c 100644 --- a/api.go +++ b/api.go @@ -116,7 +116,7 @@ func (app *appContext) checkInvites() { } app.debug.Printf("Housekeeping: Deleting old invite %s", code) notify := data.Notify - if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { + if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { app.debug.Printf("%s: Expiry notification", code) var wait sync.WaitGroup for address, settings := range notify { @@ -160,7 +160,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool if currentTime.After(expiry) { app.debug.Printf("Housekeeping: Deleting old invite %s", code) notify := inv.Notify - if app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { + if emailEnabled && 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"] { @@ -301,7 +301,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { } } } - if app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { + if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email) msg, err := app.email.constructWelcome(req.Username, app) if err != nil { @@ -332,7 +332,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc success = false return } - if app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed { + if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed { claims := jwt.MapClaims{ "valid": true, "invite": req.Code, @@ -385,7 +385,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc app.storage.loadProfiles() invite := app.storage.invites[req.Code] app.checkInvite(req.Code, true, req.Username) - if app.config.Section("notifications").Key("enabled").MustBool(false) { + if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) { for address, settings := range invite.Notify { if settings["notify-creation"] { go func() { @@ -450,7 +450,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc } } } - if app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { + if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email) msg, err := app.email.constructWelcome(req.Username, app) if err != nil { @@ -544,7 +544,7 @@ func (app *appContext) DeleteUser(gc *gin.Context) { errors[userID] += msg } } - if req.Notify { + if emailEnabled && req.Notify { addr, ok := app.storage.emails[userID] if addr != nil && ok { go func(userID, reason, address string) { @@ -611,7 +611,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { invite.RemainingUses = 1 } invite.ValidTill = validTill - if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { + if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { app.debug.Printf("%s: Sending invite email", inviteCode) invite.Email = req.Email msg, err := app.email.constructInvite(inviteCode, invite, app) @@ -1190,18 +1190,18 @@ func (app *appContext) GetConfig(gc *gin.Context) { app.info.Println("Config requested") resp := app.configBase // Load language options - formChosen, formOptions := app.storage.lang.Form.getOptions(app.config.Section("ui").Key("language-form").MustString("en-us")) + formOptions := app.storage.lang.Form.getOptions() fl := resp.Sections["ui"].Settings["language-form"] fl.Options = formOptions - fl.Value = formChosen - adminChosen, adminOptions := app.storage.lang.Admin.getOptions(app.config.Section("ui").Key("language-admin").MustString("en-us")) + fl.Value = app.config.Section("ui").Key("language-form").MustString("en-us") + adminOptions := app.storage.lang.Admin.getOptions() al := resp.Sections["ui"].Settings["language-admin"] al.Options = adminOptions - al.Value = adminChosen - emailChosen, emailOptions := app.storage.lang.Email.getOptions(app.config.Section("email").Key("language").MustString("en-us")) + al.Value = app.config.Section("ui").Key("language-admin").MustString("en-us") + emailOptions := app.storage.lang.Email.getOptions() el := resp.Sections["email"].Settings["language"] el.Options = emailOptions - el.Value = emailChosen + el.Value = app.config.Section("email").Key("language").MustString("en-us") for sectName, section := range resp.Sections { for settingName, setting := range section.Settings { val := app.config.Section(sectName).Key(settingName) @@ -1221,17 +1221,6 @@ func (app *appContext) GetConfig(gc *gin.Context) { resp.Sections["ui"].Settings["language-admin"] = al resp.Sections["email"].Settings["language"] = el - t := resp.Sections["jellyfin"].Settings["type"] - opts := make([]string, len(serverTypes)) - i := 0 - for _, v := range serverTypes { - opts[i] = v - i++ - } - t.Options = opts - t.Value = serverTypes[app.config.Section("jellyfin").Key("type").MustString("jellyfin")] - resp.Sections["jellyfin"].Settings["type"] = t - gc.JSON(200, resp) } @@ -1254,35 +1243,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { tempConfig.NewSection(section) } for setting, value := range settings.(map[string]interface{}) { - if section == "ui" && setting == "language-form" { - for key, lang := range app.storage.lang.Form { - if lang.Meta.Name == value.(string) || value.(string) == key { - tempConfig.Section("ui").Key("language-form").SetValue(key) - break - } - } - } else if section == "ui" && setting == "language-admin" { - for key, lang := range app.storage.lang.Admin { - if lang.Meta.Name == value.(string) || value.(string) == key { - tempConfig.Section("ui").Key("language-admin").SetValue(key) - break - } - } - } else if section == "email" && setting == "language" { - for key, lang := range app.storage.lang.Email { - if lang.Meta.Name == value.(string) || value.(string) == key { - tempConfig.Section("email").Key("language").SetValue(key) - break - } - } - } else if section == "jellyfin" && setting == "type" { - for k, v := range serverTypes { - if v == value.(string) { - tempConfig.Section("jellyfin").Key("type").SetValue(k) - break - } - } - } else if value.(string) != app.config.Section(section).Key(setting).MustString("") { + if value.(string) != app.config.Section(section).Key(setting).MustString("") { tempConfig.Section(section).Key(setting).SetValue(value.(string)) } } diff --git a/config.go b/config.go index e429d02..c7e083c 100644 --- a/config.go +++ b/config.go @@ -9,6 +9,8 @@ import ( "gopkg.in/ini.v1" ) +var emailEnabled = false + func (app *appContext) loadConfig() error { var err error app.config, err = ini.Load(app.configPath) @@ -55,6 +57,12 @@ func (app *appContext) loadConfig() error { app.config.Section("jellyfin").Key("device").SetValue("jfa-go") app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", VERSION, COMMIT)) + if app.config.Section("email").Key("method").MustString("") == "" { + emailEnabled = false + } else { + emailEnabled = true + } + substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("") oldFormLang := app.config.Section("ui").Key("language").MustString("") diff --git a/config/config-base.json b/config/config-base.json index 06d02bc..e72821c 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -61,8 +61,8 @@ "requires_restart": true, "type": "select", "options": [ - "jellyfin", - "emby" + ["jellyfin", "Jellyfin"], + ["emby", "Emby"] ], "value": "jellyfin", "description": "Note: Emby integration works is missing some features, such as Password Resets." @@ -90,7 +90,7 @@ "requires_restart": true, "type": "select", "options": [ - "en-us" + ["en-us", "English (US)"] ], "value": "en-us", "description": "Default Account Form Language. Submit a PR on github if you'd like to translate." @@ -101,7 +101,7 @@ "requires_restart": true, "type": "select", "options": [ - "en-us" + ["en-us", "English (US)"] ], "value": "en-us", "description": "Default Admin page Language. Settings has not been translated. Submit a PR on github if you'd like to translate." @@ -112,8 +112,8 @@ "requires_restart": true, "type": "select", "options": [ - "Jellyfin (Dark)", - "Default (Light)" + ["Jellyfin (Dark)", "Jellyfin (Dark)"], + ["Default (Light)", "Default (Light)"] ], "value": "Jellyfin (Dark)", "description": "Default appearance for all users." @@ -318,7 +318,7 @@ "order": [], "meta": { "name": "Email", - "description": "General email settings. Ignore if not using email features." + "description": "General email settings." }, "settings": { "language": { @@ -328,7 +328,7 @@ "depends_true": "method", "type": "select", "options": [ - "en-us" + ["en-us", "English (US)"] ], "value": "en-us", "description": "Default email language. Submit a PR on github if you'd like to translate." @@ -374,8 +374,9 @@ "requires_restart": false, "type": "select", "options": [ - "smtp", - "mailgun" + ["", "Disabled"], + ["smtp", "SMTP"], + ["mailgun", "Mailgun"] ], "value": "smtp", "description": "Method of sending email to use." @@ -404,7 +405,8 @@ "order": [], "meta": { "name": "Password Resets", - "description": "Settings for the password reset handler." + "description": "Settings for the password reset handler.", + "depends_true": "email|method" }, "settings": { "enabled": { @@ -457,7 +459,8 @@ "order": [], "meta": { "name": "Invite emails", - "description": "Settings for sending invites directly to users." + "description": "Settings for sending invites directly to users.", + "depends_true": "email|method" }, "settings": { "enabled": { @@ -509,7 +512,8 @@ "order": [], "meta": { "name": "Notifications", - "description": "Notification related settings." + "description": "Notification related settings.", + "depends_true": "email|method" }, "settings": { "enabled": { @@ -562,7 +566,8 @@ "order": [], "meta": { "name": "Mailgun (Email)", - "description": "Mailgun API connection settings" + "description": "Mailgun API connection settings", + "depends_true": "email|method" }, "settings": { "api_url": { @@ -585,7 +590,8 @@ "order": [], "meta": { "name": "SMTP (Email)", - "description": "SMTP Server connection settings." + "description": "SMTP Server connection settings.", + "depends_true": "email|method" }, "settings": { "username": { @@ -602,8 +608,8 @@ "requires_restart": false, "type": "select", "options": [ - "ssl_tls", - "starttls" + ["ssl_tls", "SSL/TLS"], + ["starttls", "STARTTLS"] ], "value": "starttls", "description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls." @@ -671,7 +677,8 @@ "order": [], "meta": { "name": "Welcome Emails", - "description": "Optionally send a welcome email to new users with the Jellyfin URL and their username." + "description": "Optionally send a welcome email to new users with the Jellyfin URL and their username.", + "depends_true": "email|method" }, "settings": { "enabled": { @@ -712,7 +719,8 @@ "order": [], "meta": { "name": "Email confirmation", - "description": "If enabled, a user will be sent an email confirmation link to ensure their password is right before they can make an account." + "description": "If enabled, a user will be sent an email confirmation link to ensure their password is right before they can make an account.", + "depends_true": "email|method" }, "settings": { "enabled": { @@ -752,7 +760,8 @@ "order": [], "meta": { "name": "Account Deletion", - "description": "Subject/email files for account deletion emails." + "description": "Subject/email files for account deletion emails.", + "depends_true": "email|method" }, "settings": { "subject": { diff --git a/lang.go b/lang.go index 8e8a1f5..392497c 100644 --- a/lang.go +++ b/lang.go @@ -15,15 +15,14 @@ type quantityString struct { type adminLangs map[string]adminLang -func (ls *adminLangs) getOptions(chosen string) (string, []string) { - opts := make([]string, len(*ls)) - chosenLang := (*ls)[chosen].Meta.Name +func (ls *adminLangs) getOptions() [][2]string { + opts := make([][2]string, len(*ls)) i := 0 - for _, lang := range *ls { - opts[i] = lang.Meta.Name + for key, lang := range *ls { + opts[i] = [2]string{key, lang.Meta.Name} i++ } - return chosenLang, opts + return opts } type commonLangs map[string]commonLang @@ -43,15 +42,14 @@ type adminLang struct { type formLangs map[string]formLang -func (ls *formLangs) getOptions(chosen string) (string, []string) { - opts := make([]string, len(*ls)) - chosenLang := (*ls)[chosen].Meta.Name +func (ls *formLangs) getOptions() [][2]string { + opts := make([][2]string, len(*ls)) i := 0 - for _, lang := range *ls { - opts[i] = lang.Meta.Name + for key, lang := range *ls { + opts[i] = [2]string{key, lang.Meta.Name} i++ } - return chosenLang, opts + return opts } type formLang struct { @@ -65,15 +63,14 @@ type formLang struct { type emailLangs map[string]emailLang -func (ls *emailLangs) getOptions(chosen string) (string, []string) { - opts := make([]string, len(*ls)) - chosenLang := (*ls)[chosen].Meta.Name +func (ls *emailLangs) getOptions() [][2]string { + opts := make([][2]string, len(*ls)) i := 0 - for _, lang := range *ls { - opts[i] = lang.Meta.Name + for key, lang := range *ls { + opts[i] = [2]string{key, lang.Meta.Name} i++ } - return chosenLang, opts + return opts } type emailLang struct { @@ -110,15 +107,14 @@ type setupLang struct { JSON string } -func (ls *setupLangs) getOptions(chosen string) (string, []string) { - opts := make([]string, len(*ls)) - chosenLang := (*ls)[chosen].Meta.Name +func (ls *setupLangs) getOptions() [][2]string { + opts := make([][2]string, len(*ls)) i := 0 - for _, lang := range *ls { - opts[i] = lang.Meta.Name + for key, lang := range *ls { + opts[i] = [2]string{key, lang.Meta.Name} i++ } - return chosenLang, opts + return opts } type langSection map[string]string diff --git a/models.go b/models.go index ae91990..892267a 100644 --- a/models.go +++ b/models.go @@ -138,8 +138,10 @@ type configDTO map[string]interface{} // Below are for sending config type meta struct { - Name string `json:"name"` - Description string `json:"description"` + Name string `json:"name"` + Description string `json:"description"` + DependsTrue string `json:"depends_true,omitempty"` + DependsFalse string `json:"depends_false,omitempty"` } type setting struct { @@ -149,7 +151,7 @@ type setting struct { RequiresRestart bool `json:"requires_restart"` Type string `json:"type"` // Type (string, number, bool, etc.) Value interface{} `json:"value"` - Options []string `json:"options,omitempty"` + Options [][2]string `json:"options,omitempty"` DependsTrue string `json:"depends_true,omitempty"` // If specified, this field is enabled when the specified bool setting is enabled. DependsFalse string `json:"depends_false,omitempty"` // If specified, opposite behaviour of DependsTrue. } diff --git a/pwreset.go b/pwreset.go index 7e91a47..401ddd1 100644 --- a/pwreset.go +++ b/pwreset.go @@ -42,6 +42,9 @@ type PasswordReset struct { } func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) { + if !emailEnabled { + return + } for { select { case event, ok := <-watcher.Events: diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index bf63642..3a4bc7f 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -7,6 +7,8 @@ interface settingsBoolEvent extends Event { interface Meta { name: string; description: string; + depends_true?: string; + depends_false?: string; } interface Setting { @@ -16,13 +18,21 @@ interface Setting { requires_restart: boolean; type: string; value: string | boolean | number; - depends_true?: Setting; - depends_false?: Setting; + depends_true?: string; + depends_false?: string; asElement: () => HTMLElement; update: (s: Setting) => void; } +const splitDependant = (section: string, dep: string): string[] => { + let parts = dep.split("|"); + if (parts.length == 1) { + parts = [section, parts[0]]; + } + return parts +}; + class DOMInput { protected _input: HTMLInputElement; private _container: HTMLDivElement; @@ -76,7 +86,7 @@ class DOMInput { - + `; this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement; @@ -84,11 +94,15 @@ class DOMInput { this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement; this._input = this._container.querySelector("input[type=" + inputType + "]") as HTMLInputElement; if (setting.depends_false || setting.depends_true) { - let dependant = setting.depends_true || setting.depends_false; + let dependant = splitDependant(section, setting.depends_true || setting.depends_false); let state = true; if (setting.depends_false) { state = false; } - document.addEventListener(`settings-${section}-${dependant}`, (event: settingsBoolEvent) => { - this._input.disabled = (event.detail !== state); + document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => { + if (Boolean(event.detail) !== state) { + this._input.parentElement.classList.add("unfocused"); + } else { + this._input.parentElement.classList.remove("unfocused"); + } }); } const onValueChange = () => { @@ -206,7 +220,7 @@ class DOMBool implements SBool { this._container = document.createElement("div"); this._container.classList.add("setting"); this._container.innerHTML = ` -