diff --git a/api.go b/api.go index ab6b570..5b0aeb8 100644 --- a/api.go +++ b/api.go @@ -445,6 +445,14 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc app.info.Printf("%s: Sent welcome email to %s", req.Username, req.Email) } } + if invite.UserExpiry { + expiry := time.Now().Add(time.Duration(60*(invite.UserDays*24+invite.UserHours)+invite.UserMinutes) * time.Minute) + app.storage.users[id] = expiry + err := app.storage.storeUsers() + if err != nil { + app.err.Printf("Failed to store user duration: %s", err) + } + } success = true return } @@ -634,8 +642,8 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { } else { invite.RemainingUses = 1 } - invite.UserDuration = req.UserDuration - if invite.UserDuration { + invite.UserExpiry = req.UserExpiry + if invite.UserExpiry { invite.UserDays = req.UserDays invite.UserHours = req.UserHours invite.UserMinutes = req.UserMinutes @@ -819,18 +827,18 @@ func (app *appContext) GetInvites(gc *gin.Context) { for code, inv := range app.storage.invites { _, _, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime) invite := inviteDTO{ - Code: code, - Days: days, - Hours: hours, - Minutes: minutes, - UserDuration: inv.UserDuration, - UserDays: inv.UserDays, - UserHours: inv.UserHours, - UserMinutes: inv.UserMinutes, - Created: app.formatDatetime(inv.Created), - Profile: inv.Profile, - NoLimit: inv.NoLimit, - Label: inv.Label, + Code: code, + Days: days, + Hours: hours, + Minutes: minutes, + UserExpiry: inv.UserExpiry, + UserDays: inv.UserDays, + UserHours: inv.UserHours, + UserMinutes: inv.UserMinutes, + Created: app.formatDatetime(inv.Created), + Profile: inv.Profile, + NoLimit: inv.NoLimit, + Label: inv.Label, } if len(inv.UsedBy) != 0 { invite.UsedBy = inv.UsedBy @@ -1281,18 +1289,24 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { // @Summary Get a list of email names and IDs. // @Produce json +// @Param lang query string false "Language for email titles." // @Success 200 {object} emailListDTO // @Router /config/emails [get] // @tags Configuration func (app *appContext) GetEmails(gc *gin.Context) { + lang := gc.Query("lang") + if _, ok := app.storage.lang.Email[lang]; !ok { + lang = app.storage.lang.chosenEmailLang + } gc.JSON(200, emailListDTO{ - "UserCreated": {Name: app.storage.lang.Email["en-us"].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled}, - "InviteExpiry": {Name: app.storage.lang.Email["en-us"].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled}, - "PasswordReset": {Name: app.storage.lang.Email["en-us"].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled}, - "UserDeleted": {Name: app.storage.lang.Email["en-us"].UserDeleted["name"], Enabled: app.storage.customEmails.UserDeleted.Enabled}, - "InviteEmail": {Name: app.storage.lang.Email["en-us"].InviteEmail["name"], Enabled: app.storage.customEmails.InviteEmail.Enabled}, - "WelcomeEmail": {Name: app.storage.lang.Email["en-us"].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled}, - "EmailConfirmation": {Name: app.storage.lang.Email["en-us"].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled}, + "UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled}, + "InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled}, + "PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled}, + "UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.customEmails.UserDeleted.Enabled}, + "InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.customEmails.InviteEmail.Enabled}, + "WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled}, + "EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled}, + "UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.customEmails.UserExpired.Enabled}, }) } @@ -1333,6 +1347,9 @@ func (app *appContext) SetEmail(gc *gin.Context) { } else if id == "EmailConfirmation" { app.storage.customEmails.EmailConfirmation.Content = req.Content app.storage.customEmails.EmailConfirmation.Enabled = true + } else if id == "UserExpired" { + app.storage.customEmails.UserExpired.Content = req.Content + app.storage.customEmails.UserExpired.Enabled = true } else { respondBool(400, false, gc) return @@ -1374,6 +1391,8 @@ func (app *appContext) SetEmailState(gc *gin.Context) { app.storage.customEmails.WelcomeEmail.Enabled = enabled } else if id == "EmailConfirmation" { app.storage.customEmails.EmailConfirmation.Enabled = enabled + } else if id == "UserExpired" { + app.storage.customEmails.UserExpired.Enabled = enabled } else { respondBool(400, false, gc) return @@ -1393,6 +1412,7 @@ func (app *appContext) SetEmailState(gc *gin.Context) { // @Router /config/emails/{id} [get] // @tags Configuration func (app *appContext) GetEmail(gc *gin.Context) { + lang := app.storage.lang.chosenEmailLang id := gc.Param("id") var content string var err error @@ -1401,8 +1421,8 @@ func (app *appContext) GetEmail(gc *gin.Context) { var values map[string]interface{} var writeVars func(variables []string) newEmail := false - username := app.storage.lang.Email[app.storage.lang.chosenEmailLang].Strings.get("username") - emailAddress := app.storage.lang.Email[app.storage.lang.chosenEmailLang].Strings.get("emailAddress") + username := app.storage.lang.Email[lang].Strings.get("username") + emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress") if id == "UserCreated" { content = app.storage.customEmails.UserCreated.Content if content == "" { @@ -1449,7 +1469,7 @@ func (app *appContext) GetEmail(gc *gin.Context) { variables = app.storage.customEmails.UserDeleted.Variables } writeVars = func(variables []string) { app.storage.customEmails.UserDeleted.Variables = variables } - values = app.email.deletedValues(app.storage.lang.Email[app.storage.lang.chosenEmailLang].UserDeleted.get("reason"), app, false) + values = app.email.deletedValues(app.storage.lang.Email[lang].UserDeleted.get("reason"), app, false) // app.storage.customEmails.UserDeleted = content } else if id == "InviteEmail" { content = app.storage.customEmails.InviteEmail.Content @@ -1487,6 +1507,17 @@ func (app *appContext) GetEmail(gc *gin.Context) { writeVars = func(variables []string) { app.storage.customEmails.EmailConfirmation.Variables = variables } values = app.email.confirmationValues("xxxxxx", username, "xxxxxx", app, false) // app.storage.customEmails.EmailConfirmation = content + } else if id == "UserExpired" { + content = app.storage.customEmails.UserExpired.Content + if content == "" { + newEmail = true + msg, err = app.email.constructUserExpired(app, true) + content = msg.Text + } else { + variables = app.storage.customEmails.UserExpired.Variables + } + writeVars = func(variables []string) { app.storage.customEmails.UserExpired.Variables = variables } + values = app.email.userExpiredValues(app, false) } else { respondBool(400, false, gc) return @@ -1515,6 +1546,9 @@ func (app *appContext) GetEmail(gc *gin.Context) { } writeVars(variables) } + if variables == nil { + variables = []string{} + } if app.storage.storeCustomEmails() != nil { respondBool(500, false, gc) return diff --git a/config.go b/config.go index 6c96c3e..e938340 100644 --- a/config.go +++ b/config.go @@ -36,7 +36,7 @@ func (app *appContext) loadConfig() error { key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json")))) } } - for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails"} { + for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users"} { app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json")))) } app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/") @@ -66,6 +66,10 @@ func (app *appContext) loadConfig() error { app.config.Section("template_email").Key("email_html").SetValue(app.config.Section("template_email").Key("email_html").MustString("jfa-go:" + "template.html")) app.config.Section("template_email").Key("email_text").SetValue(app.config.Section("template_email").Key("email_text").MustString("jfa-go:" + "template.txt")) + app.config.Section("user_expiry").Key("behaviour").SetValue(app.config.Section("user_expiry").Key("behaviour").MustString("disable_user")) + app.config.Section("user_expiry").Key("email_html").SetValue(app.config.Section("user_expiry").Key("email_html").MustString("jfa-go:" + "user-expired.html")) + app.config.Section("user_expiry").Key("email_text").SetValue(app.config.Section("user_expiry").Key("email_text").MustString("jfa-go:" + "user-expired.txt")) + app.config.Section("jellyfin").Key("version").SetValue(version) 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)) diff --git a/config/config-base.json b/config/config-base.json index a681639..4d39470 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -765,6 +765,63 @@ } } }, + "user_expiry": { + "order": [], + "meta": { + "name": "User Expiry", + "description": "When set on an invite, users will be deleted or disabled a specified amount of time after they create their account." + }, + "settings": { + "behaviour": { + "name": "Behaviour", + "required": false, + "requires_restart": false, + "type": "select", + "options": [ + ["delete_user", "Delete user"], + ["disable_user", "Disable user"] + ], + "value": "disable_user", + "description": "Whether to delete or disable users on expiry." + }, + "send_email": { + "name": "Send email", + "required": false, + "requires_restart": false, + "type": "bool", + "value": true, + "depends_true": "email|method", + "description": "Send an email when a user's account expires." + }, + "subject": { + "name": "Email subject", + "required": false, + "requires_restart": false, + "depends_true": "email|method", + "type": "text", + "value": "", + "description": "Subject of user expiry emails." + }, + "email_html": { + "name": "Custom email (HTML)", + "required": false, + "requires_restart": false, + "depends_true": "email|method", + "type": "text", + "value": "", + "description": "Path to custom email html" + }, + "email_text": { + "name": "Custom email (plaintext)", + "required": false, + "requires_restart": false, + "depends_true": "email|method", + "type": "text", + "value": "", + "description": "Path to custom email in plain text" + } + } + }, "deletion": { "order": [], "meta": { @@ -822,6 +879,14 @@ "value": "", "description": "Location of stored email addresses (json)." }, + "users": { + "name": "User storage", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "Stores users temporarily when a user expiry is set." + }, "ombi_template": { "name": "Ombi user template", "required": false, diff --git a/daemon.go b/daemon.go index 51299dc..397f0a2 100644 --- a/daemon.go +++ b/daemon.go @@ -4,7 +4,7 @@ import "time" // https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS -type repeater struct { +type inviteDaemon struct { Stopped bool ShutdownChannel chan string Interval time.Duration @@ -12,8 +12,8 @@ type repeater struct { app *appContext } -func newRepeater(interval time.Duration, app *appContext) *repeater { - return &repeater{ +func newInviteDaemon(interval time.Duration, app *appContext) *inviteDaemon { + return &inviteDaemon{ Stopped: false, ShutdownChannel: make(chan string), Interval: interval, @@ -22,7 +22,7 @@ func newRepeater(interval time.Duration, app *appContext) *repeater { } } -func (rt *repeater) run() { +func (rt *inviteDaemon) run() { rt.app.info.Println("Invite daemon started") for { select { @@ -42,7 +42,7 @@ func (rt *repeater) run() { } } -func (rt *repeater) shutdown() { +func (rt *inviteDaemon) shutdown() { rt.Stopped = true rt.ShutdownChannel <- "Down" <-rt.ShutdownChannel diff --git a/email.go b/email.go index bc74060..730e5e5 100644 --- a/email.go +++ b/email.go @@ -569,6 +569,42 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub return email, nil } +func (emailer *Emailer) userExpiredValues(app *appContext, noSub bool) map[string]interface{} { + template := map[string]interface{}{ + "yourAccountHasExpired": emailer.lang.UserExpired.get("yourAccountHasExpired"), + "contactTheAdmin": emailer.lang.UserExpired.get("contactTheAdmin"), + "message": "", + } + if !noSub { + template["message"] = app.config.Section("email").Key("message").String() + } + return template +} + +func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Email, error) { + email := &Email{ + Subject: app.config.Section("user_expiry").Key("subject").MustString(emailer.lang.UserExpired.get("title")), + } + var err error + template := emailer.userExpiredValues(app, noSub) + if app.storage.customEmails.UserExpired.Enabled { + content := app.storage.customEmails.UserExpired.Content + for _, v := range app.storage.customEmails.UserExpired.Variables { + replaceWith, ok := template[v[1:len(v)-1]] + if ok { + content = strings.ReplaceAll(content, v, replaceWith.(string)) + } + } + email, err = emailer.constructTemplate(email.Subject, content, app) + } else { + email.HTML, email.Text, err = emailer.construct(app, "user_expiry", "email_", template) + } + if err != nil { + return nil, err + } + return email, nil +} + // calls the send method in the underlying emailClient. func (emailer *Emailer) send(email *Email, address ...string) error { return emailer.sender.send(emailer.fromName, emailer.fromAddr, email, address...) diff --git a/html/admin.html b/html/admin.html index 32bc995..4b2a270 100644 --- a/html/admin.html +++ b/html/admin.html @@ -9,6 +9,7 @@ window.ombiEnabled = {{ .ombiEnabled }}; window.usernameEnabled = {{ .username }}; window.langFile = JSON.parse({{ .language }}); + window.language = "{{ .langName }}"; {{ template "header.html" . }} Admin - jfa-go @@ -132,7 +133,7 @@ ×
- {{ .strings.variables }} + {{ .strings.variables }}
@@ -265,8 +266,8 @@ {{ .strings.inviteDuration }}
@@ -289,11 +290,11 @@
-
-

{{ .strings.userDurationDescription }}

+
+

{{ .strings.userExpiryDescription }}

-
diff --git a/html/form-base.html b/html/form-base.html index 0b2fe9c..02ad5a6 100644 --- a/html/form-base.html +++ b/html/form-base.html @@ -7,11 +7,11 @@ window.code = "{{ .code }}"; window.messages = JSON.parse({{ .notifications }}); window.confirmation = {{ .confirmation }}; - window.userDurationEnabled = {{ .userDuration }}; - window.userDurationDays = {{ .userDurationDays }}; - window.userDurationHours = {{ .userDurationHours }}; - window.userDurationMinutes = {{ .userDurationMinutes }}; - window.userDurationMessage = {{ .userDurationMessage }}; + window.userExpiryEnabled = {{ .userExpiry }}; + window.userExpiryDays = {{ .userExpiryDays }}; + window.userExpiryHours = {{ .userExpiryHours }}; + window.userExpiryMinutes = {{ .userExpiryMinutes }}; + window.userExpiryMessage = {{ .userExpiryMessage }}; {{ end }} diff --git a/html/form.html b/html/form.html index a446871..9fe6694 100644 --- a/html/form.html +++ b/html/form.html @@ -37,8 +37,8 @@
- {{ if .userDuration }} - + {{ if .userExpiry }} + {{ end }}
- + ${window.lang.strings("delete")}