mirror of
https://github.com/hrfee/jfa-go.git
synced 2025-04-19 09:32:53 +00:00
Compare commits
4 Commits
3635b6a367
...
fa7da1b23f
Author | SHA1 | Date | |
---|---|---|---|
fa7da1b23f | |||
1ec5d2ca3f | |||
1e9d184508 | |||
2934832a98 |
121
api.go
121
api.go
@ -445,10 +445,48 @@ 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
|
||||
}
|
||||
|
||||
// @Summary Extend time before the user(s) expiry.
|
||||
// @Produce json
|
||||
// @Param extendExpiryDTO body extendExpiryDTO true "Extend expiry object"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Router /users/extend [post]
|
||||
// @tags Users
|
||||
func (app *appContext) ExtendExpiry(gc *gin.Context) {
|
||||
var req extendExpiryDTO
|
||||
app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users))
|
||||
gc.BindJSON(&req)
|
||||
if req.Days == 0 && req.Hours == 0 && req.Minutes == 0 {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
for _, id := range req.Users {
|
||||
if expiry, ok := app.storage.users[id]; ok {
|
||||
app.storage.users[id] = expiry.Add(time.Duration(60*(req.Days*24+req.Hours)+req.Minutes) * time.Minute)
|
||||
app.debug.Printf("Expiry extended for \"%s\"", id)
|
||||
}
|
||||
}
|
||||
if err := app.storage.storeUsers(); err != nil {
|
||||
app.err.Printf("Failed to store user duration: %s", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(204, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Creates a new Jellyfin user via invite code
|
||||
// @Produce json
|
||||
// @Param newUserDTO body newUserDTO true "New user request object"
|
||||
@ -634,6 +672,12 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
} else {
|
||||
invite.RemainingUses = 1
|
||||
}
|
||||
invite.UserExpiry = req.UserExpiry
|
||||
if invite.UserExpiry {
|
||||
invite.UserDays = req.UserDays
|
||||
invite.UserHours = req.UserHours
|
||||
invite.UserMinutes = req.UserMinutes
|
||||
}
|
||||
invite.ValidTill = validTill
|
||||
if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||
app.debug.Printf("%s: Sending invite email", inviteCode)
|
||||
@ -750,6 +794,7 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
|
||||
app.info.Println("Profile creation requested")
|
||||
var req newProfileDTO
|
||||
gc.BindJSON(&req)
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
user, status, err := app.jf.UserByID(req.ID, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get user from Jellyfin (%d): %s", status, err)
|
||||
@ -813,14 +858,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,
|
||||
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
|
||||
@ -980,9 +1029,10 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
}
|
||||
for _, jfUser := range users {
|
||||
user := respUser{
|
||||
ID: jfUser.ID,
|
||||
Name: jfUser.Name,
|
||||
Admin: jfUser.Policy.IsAdministrator,
|
||||
ID: jfUser.ID,
|
||||
Name: jfUser.Name,
|
||||
Admin: jfUser.Policy.IsAdministrator,
|
||||
Disabled: jfUser.Policy.IsDisabled,
|
||||
}
|
||||
user.LastActive = "n/a"
|
||||
if !jfUser.LastActivityDate.IsZero() {
|
||||
@ -991,6 +1041,9 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
if email, ok := app.storage.emails[jfUser.ID]; ok {
|
||||
user.Email = email.(string)
|
||||
}
|
||||
if expiry, ok := app.storage.users[jfUser.ID]; ok {
|
||||
user.Expiry = app.formatDatetime(expiry)
|
||||
}
|
||||
|
||||
resp.UserList = append(resp.UserList, user)
|
||||
}
|
||||
@ -1271,18 +1324,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},
|
||||
})
|
||||
}
|
||||
|
||||
@ -1323,6 +1382,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
|
||||
@ -1364,6 +1426,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
|
||||
@ -1383,6 +1447,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
|
||||
@ -1391,8 +1456,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 == "" {
|
||||
@ -1439,7 +1504,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
|
||||
@ -1477,6 +1542,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
|
||||
@ -1505,6 +1581,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
|
||||
|
44
config.go
44
config.go
@ -22,6 +22,10 @@ func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
||||
return os.DirFS(dir), file
|
||||
}
|
||||
|
||||
func (app *appContext) MustSetValue(section, key, val string) {
|
||||
app.config.Section(section).Key(key).SetValue(app.config.Section(section).Key(key).MustString(val))
|
||||
}
|
||||
|
||||
func (app *appContext) loadConfig() error {
|
||||
var err error
|
||||
app.config, err = ini.Load(app.configPath)
|
||||
@ -29,42 +33,46 @@ func (app *appContext) loadConfig() error {
|
||||
return err
|
||||
}
|
||||
|
||||
app.config.Section("jellyfin").Key("public_server").SetValue(app.config.Section("jellyfin").Key("public_server").MustString(app.config.Section("jellyfin").Key("server").String()))
|
||||
app.MustSetValue("jellyfin", "public_server", app.config.Section("jellyfin").Key("server").String())
|
||||
|
||||
for _, key := range app.config.Section("files").Keys() {
|
||||
if name := key.Name(); name != "html_templates" && name != "lang_files" {
|
||||
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(""), "/")
|
||||
app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false)))
|
||||
|
||||
app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString("jfa-go:" + "email.html"))
|
||||
app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString("jfa-go:" + "email.txt"))
|
||||
app.MustSetValue("password_resets", "email_html", "jfa-go:"+"email.html")
|
||||
app.MustSetValue("password_resets", "email_text", "jfa-go:"+"email.txt")
|
||||
|
||||
app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString("jfa-go:" + "invite-email.html"))
|
||||
app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString("jfa-go:" + "invite-email.txt"))
|
||||
app.MustSetValue("invite_emails", "email_html", "jfa-go:"+"invite-email.html")
|
||||
app.MustSetValue("invite_emails", "email_text", "jfa-go:"+"invite-email.txt")
|
||||
|
||||
app.config.Section("email_confirmation").Key("email_html").SetValue(app.config.Section("email_confirmation").Key("email_html").MustString("jfa-go:" + "confirmation.html"))
|
||||
app.config.Section("email_confirmation").Key("email_text").SetValue(app.config.Section("email_confirmation").Key("email_text").MustString("jfa-go:" + "confirmation.txt"))
|
||||
app.MustSetValue("email_confirmation", "email_html", "jfa-go:"+"confirmation.html")
|
||||
app.MustSetValue("email_confirmation", "email_text", "jfa-go:"+"confirmation.txt")
|
||||
|
||||
app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString("jfa-go:" + "expired.html"))
|
||||
app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString("jfa-go:" + "expired.txt"))
|
||||
app.MustSetValue("notifications", "expiry_html", "jfa-go:"+"expired.html")
|
||||
app.MustSetValue("notifications", "expiry_text", "jfa-go:"+"expired.txt")
|
||||
|
||||
app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString("jfa-go:" + "created.html"))
|
||||
app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString("jfa-go:" + "created.txt"))
|
||||
app.MustSetValue("notifications", "created_html", "jfa-go:"+"created.html")
|
||||
app.MustSetValue("notifications", "created_text", "jfa-go:"+"created.txt")
|
||||
|
||||
app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString("jfa-go:" + "deleted.html"))
|
||||
app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString("jfa-go:" + "deleted.txt"))
|
||||
app.MustSetValue("deletion", "email_html", "jfa-go:"+"deleted.html")
|
||||
app.MustSetValue("deletion", "email_text", "jfa-go:"+"deleted.txt")
|
||||
|
||||
app.config.Section("welcome_email").Key("email_html").SetValue(app.config.Section("welcome_email").Key("email_html").MustString("jfa-go:" + "welcome.html"))
|
||||
app.config.Section("welcome_email").Key("email_text").SetValue(app.config.Section("welcome_email").Key("email_text").MustString("jfa-go:" + "welcome.txt"))
|
||||
app.MustSetValue("welcome_email", "email_html", "jfa-go:"+"welcome.html")
|
||||
app.MustSetValue("welcome_email", "email_text", "jfa-go:"+"welcome.txt")
|
||||
|
||||
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.MustSetValue("template_email", "email_html", "jfa-go:"+"template.html")
|
||||
app.MustSetValue("template_email", "email_text", "jfa-go:"+"template.txt")
|
||||
|
||||
app.MustSetValue("user_expiry", "behaviour", "disable_user")
|
||||
app.MustSetValue("user_expiry", "email_html", "jfa-go:"+"user-expired.html")
|
||||
app.MustSetValue("user_expiry", "email_text", "jfa-go:"+"user-expired.txt")
|
||||
|
||||
app.config.Section("jellyfin").Key("version").SetValue(version)
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
|
@ -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,
|
||||
|
10
daemon.go
10
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
|
||||
|
36
email.go
36
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...)
|
||||
|
111
html/admin.html
111
html/admin.html
@ -9,6 +9,7 @@
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
window.language = "{{ .langName }}";
|
||||
</script>
|
||||
{{ template "header.html" . }}
|
||||
<title>Admin - jfa-go</title>
|
||||
@ -93,6 +94,35 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-extend-expiry" class="modal">
|
||||
<form class="modal-content card" id="form-extend-expiry" href="">
|
||||
<span class="heading"><span id="header-extend-expiry"></span> <span class="modal-close">×</span></span>
|
||||
<div class="content mt-half">
|
||||
<label class="label supra" for="extend-expiry-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="extend-expiry-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="label supra" for="extend-expiry-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="extend-expiry-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="label supra" for="extend-expiry-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="extend-expiry-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~critical !normal full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-announce" class="modal">
|
||||
<form class="modal-content card" id="form-announce" href="">
|
||||
<span class="heading"><span id="header-announce"></span> <span class="modal-close">×</span></span>
|
||||
@ -132,7 +162,7 @@
|
||||
<span class="heading"><span id="header-editor"></span> <span class="modal-close">×</span></span>
|
||||
<div class="row">
|
||||
<div class="col flex-col content mt-half">
|
||||
<span class="label supra" for="editor-variables">{{ .strings.variables }}</span>
|
||||
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
|
||||
<div id="editor-variables"></div>
|
||||
<label class="label supra" for="textarea-editor">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-editor" class="textarea full-width flex-auto ~neutral !normal mt-half monospace"></textarea>
|
||||
@ -259,23 +289,62 @@
|
||||
<span class="heading">{{ .strings.create }}</span>
|
||||
<div class="row" id="create-inv">
|
||||
<div class="card ~neutral !normal col">
|
||||
<label class="label supra" for="create-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div class="flex-row mb-1">
|
||||
<label class="flex-row-group mr-1">
|
||||
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
|
||||
<span class="button ~neutral !high supra full-width center">{{ .strings.inviteDuration }}</span>
|
||||
</label>
|
||||
<label class="flex-row-group ml-1">
|
||||
<input type="radio" name="duration" class="unfocused" id="radio-user-expiry">
|
||||
<span class="button ~neutral !normal supra full-width center">{{ .strings.userExpiry }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<label class="label supra" for="create-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div id="inv-duration">
|
||||
<label class="label supra" for="create-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="label supra" for="create-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="label supra" for="create-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label class="label supra" for="create-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div id="user-expiry" class="unfocused">
|
||||
<p class="support">{{ .strings.userExpiryDescription }}</p>
|
||||
<div class="mb-half">
|
||||
<label for="create-user-expiry-enabled" class="button ~neutral !normal">
|
||||
<input type="checkbox" id="create-user-expiry-enabled" aria-label="User duration enabled">
|
||||
<span class="ml-half">{{ .strings.enabled }} </span>
|
||||
</label>
|
||||
</div>
|
||||
<label class="label supra" for="user-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="user-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="label supra" for="user-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="user-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="label supra" for="user-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="user-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label class="label supra" for="create-label"> {{ .strings.label }}</label>
|
||||
<input type="text" id="create-label" class="input ~neutral !normal mb-1 mt-half">
|
||||
@ -313,10 +382,11 @@
|
||||
<div class="card ~neutral !low accounts mb-1">
|
||||
<span class="heading">{{ .strings.accounts }}</span>
|
||||
<div class="fr">
|
||||
<span class="button ~neutral !normal" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
||||
<span class="button ~info !normal" id="accounts-announce">{{ .strings.announce }}</span>
|
||||
<span class="button ~urge !normal" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
||||
<span class="button ~critical !normal" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||
<span class="button ~neutral !normal mb-half" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
||||
<span class="button ~info !normal mb-half" id="accounts-announce">{{ .strings.announce }}</span>
|
||||
<span class="button ~urge !normal mb-half" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
||||
<span class="button ~warning !normal mb-half" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
|
||||
<span class="button ~critical !normal mb-half" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||
</div>
|
||||
<div class="card ~neutral !normal accounts-header table-responsive mt-half">
|
||||
<table class="table">
|
||||
@ -325,6 +395,7 @@
|
||||
<th><input type="checkbox" value="" id="accounts-select-all"></th>
|
||||
<th>{{ .strings.username }}</th>
|
||||
<th>{{ .strings.emailAddress }}</th>
|
||||
<th>{{ .strings.expiry }}</th>
|
||||
<th>{{ .strings.lastActiveTime }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -7,6 +7,11 @@
|
||||
window.code = "{{ .code }}";
|
||||
window.messages = JSON.parse({{ .notifications }});
|
||||
window.confirmation = {{ .confirmation }};
|
||||
window.userExpiryEnabled = {{ .userExpiry }};
|
||||
window.userExpiryDays = {{ .userExpiryDays }};
|
||||
window.userExpiryHours = {{ .userExpiryHours }};
|
||||
window.userExpiryMinutes = {{ .userExpiryMinutes }};
|
||||
window.userExpiryMessage = {{ .userExpiryMessage }};
|
||||
</script>
|
||||
<script src="js/form.js" type="module"></script>
|
||||
{{ end }}
|
||||
|
@ -37,6 +37,9 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ if .userExpiry }}
|
||||
<aside class="col aside sm ~warning" id="user-expiry-message"></aside>
|
||||
{{ end }}
|
||||
<form class="card ~neutral !normal" id="form-create" href="">
|
||||
<label class="label supra">
|
||||
{{ .strings.username }}
|
||||
@ -44,13 +47,13 @@
|
||||
</label>
|
||||
|
||||
<label class="label supra" for="create-email">{{ .strings.emailAddress }}</label>
|
||||
<input type="email" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .strings.emailAddress }}" id="create-email" aria-label="{{ .strings.emailAddress }}" value="{{ .email }}">
|
||||
<input type="email" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.emailAddress }}" id="create-email" aria-label="{{ .strings.emailAddress }}" value="{{ .email }}">
|
||||
|
||||
<label class="label supra" for="create-password">{{ .strings.password }}</label>
|
||||
<input type="password" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-password" aria-label="{{ .strings.password }}">
|
||||
<input type="password" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-password" aria-label="{{ .strings.password }}">
|
||||
|
||||
<label class="label supra" for="create-reenter-password">{{ .strings.reEnterPassword }}</label>
|
||||
<input type="password" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-reenter-password" aria-label="{{ .strings.reEnterPassword }}">
|
||||
<input type="password" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-reenter-password" aria-label="{{ .strings.reEnterPassword }}">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.createAccountButton }}</span>
|
||||
|
1
lang.go
1
lang.go
@ -83,6 +83,7 @@ type emailLang struct {
|
||||
InviteEmail langSection `json:"inviteEmail"`
|
||||
WelcomeEmail langSection `json:"welcomeEmail"`
|
||||
EmailConfirmation langSection `json:"emailConfirmation"`
|
||||
UserExpired langSection `json:"userExpired"`
|
||||
}
|
||||
|
||||
type setupLangs map[string]setupLang
|
||||
|
@ -10,6 +10,7 @@
|
||||
"inviteHours": "Hours",
|
||||
"inviteMinutes": "Minutes",
|
||||
"inviteNumberOfUses": "Number of uses",
|
||||
"inviteDuration": "Invite Duration",
|
||||
"warning": "Warning",
|
||||
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
|
||||
"inviteSendToEmail": "Send to",
|
||||
@ -20,9 +21,15 @@
|
||||
"delete": "Delete",
|
||||
"name": "Name",
|
||||
"date": "Date",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"admin": "Admin",
|
||||
"lastActiveTime": "Last Active",
|
||||
"from": "From",
|
||||
"user": "User",
|
||||
"expiry": "Expiry",
|
||||
"userExpiry": "User Expiry",
|
||||
"userExpiryDescription": "A specified amount of time after each signup, jfa-go will delete/disable the account. You can change this behaviour in settings.",
|
||||
"aboutProgram": "About",
|
||||
"version": "Version",
|
||||
"commitNoun": "Commit",
|
||||
@ -37,6 +44,7 @@
|
||||
"preview": "Preview",
|
||||
"reset": "Reset",
|
||||
"edit": "Edit",
|
||||
"extendExpiry": "Extend expiry",
|
||||
"customizeEmails": "Customize Emails",
|
||||
"customizeEmailsDescription": "If you don't want to use jfa-go's email templates, you can create your own using Markdown.",
|
||||
"markdownSupported": "Markdown is supported.",
|
||||
@ -137,6 +145,14 @@
|
||||
"appliedSettings": {
|
||||
"singular": "Applied settings to {n} user.",
|
||||
"plural": "Applied settings to {n} users."
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Extend expiry for {n} user",
|
||||
"plural": "Extend expiry for {n} users"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Extended expiry for {n} user.",
|
||||
"plural": "Extended expiry for {n} users."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,5 +55,11 @@
|
||||
"title": "Confirm your email - Jellyfin",
|
||||
"clickBelow": "Click the link below to confirm your email address and start using Jellyfin.",
|
||||
"confirmEmail": "Confirm Email"
|
||||
},
|
||||
"userExpired": {
|
||||
"name": "User expiry",
|
||||
"title": "Your account has expired - Jellyfin",
|
||||
"yourAccountHasExpired": "Your account has expired.",
|
||||
"contactTheAdmin": "Contact the administrator for more info."
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,8 @@
|
||||
"successHeader": "Success!",
|
||||
"successContinueButton": "Continue",
|
||||
"confirmationRequired": "Email confirmation required",
|
||||
"confirmationRequiredMessage": "Please check your email inbox to verify your address."
|
||||
"confirmationRequiredMessage": "Please check your email inbox to verify your address.",
|
||||
"yourAccountIsValidUntil": "Your account will be valid until {date}."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "User already exists.",
|
||||
|
76
mail/user-expired.mjml
Normal file
76
mail/user-expired.mjml
Normal file
@ -0,0 +1,76 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-raw>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
</mj-raw>
|
||||
<mj-style>
|
||||
:root {
|
||||
Color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
</mj-style>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="#cacaca" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
</mj-attributes>
|
||||
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> {{ .jellyfin }} </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<h3>{{ .yourAccountHasExpired }}</h3>
|
||||
<p>{{ .contactTheAdmin }}</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||
{{ .message }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</body>
|
||||
</mjml>
|
5
mail/user-expired.txt
Normal file
5
mail/user-expired.txt
Normal file
@ -0,0 +1,5 @@
|
||||
{{ .yourAccountHasExpired }}
|
||||
|
||||
{{ .contactTheAdmin }}
|
||||
|
||||
{{ .message }}
|
7
main.go
7
main.go
@ -338,6 +338,8 @@ func start(asDaemon, firstCall bool) {
|
||||
app.storage.loadConfiguration()
|
||||
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
|
||||
app.storage.loadDisplayprefs()
|
||||
app.storage.users_path = app.config.Section("files").Key("users").String()
|
||||
app.storage.loadUsers()
|
||||
|
||||
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
||||
app.storage.loadProfiles()
|
||||
@ -510,9 +512,12 @@ func start(asDaemon, firstCall bool) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
inviteDaemon := newRepeater(time.Duration(60*time.Second), app)
|
||||
inviteDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
|
||||
go inviteDaemon.run()
|
||||
|
||||
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
|
||||
go userDaemon.run()
|
||||
|
||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
|
||||
go app.StartPWR()
|
||||
}
|
||||
|
35
models.go
35
models.go
@ -30,15 +30,19 @@ type deleteUserDTO struct {
|
||||
}
|
||||
|
||||
type generateInviteDTO struct {
|
||||
Days int `json:"days" example:"1"` // Number of days
|
||||
Hours int `json:"hours" example:"2"` // Number of hours
|
||||
Minutes int `json:"minutes" example:"3"` // Number of minutes
|
||||
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address
|
||||
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
||||
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
||||
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
||||
Profile string `json:"profile" example:"DefaultProfile"` // Name of profile to apply on this invite
|
||||
Label string `json:"label" example:"For Friends"` // Optional label for the invite
|
||||
Days int `json:"days" example:"1"` // Number of days
|
||||
Hours int `json:"hours" example:"2"` // Number of hours
|
||||
Minutes int `json:"minutes" example:"3"` // Number of minutes
|
||||
UserExpiry bool `json:"user-expiry"` // Whether or not user expiry is enabled
|
||||
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
||||
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
||||
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
||||
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address
|
||||
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
||||
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
||||
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
||||
Profile string `json:"profile" example:"DefaultProfile"` // Name of profile to apply on this invite
|
||||
Label string `json:"label" example:"For Friends"` // Optional label for the invite
|
||||
}
|
||||
|
||||
type inviteProfileDTO struct {
|
||||
@ -72,6 +76,10 @@ type inviteDTO struct {
|
||||
Days int `json:"days" example:"1"` // Number of days till expiry
|
||||
Hours int `json:"hours" example:"2"` // Number of hours till expiry
|
||||
Minutes int `json:"minutes" example:"3"` // Number of minutes till expiry
|
||||
UserExpiry bool `json:"user-expiry"` // Whether or not user expiry is enabled
|
||||
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
||||
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
||||
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
||||
Created string `json:"created" example:"01/01/20 12:00"` // Date of creation
|
||||
Profile string `json:"profile" example:"DefaultProfile"` // Profile used on this invite
|
||||
UsedBy [][]string `json:"used-by,omitempty"` // Users who have used this invite
|
||||
@ -106,6 +114,8 @@ type respUser struct {
|
||||
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
|
||||
LastActive string `json:"last_active"` // Time of last activity on Jellyfin
|
||||
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
||||
Expiry string `json:"expiry" example:"01/02/21 12:00"` // Expiry time of user, if applicable.
|
||||
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
|
||||
}
|
||||
|
||||
type getUsersDTO struct {
|
||||
@ -197,3 +207,10 @@ type customEmailDTO struct {
|
||||
HTML string `json:"html"`
|
||||
Plaintext string `json:"plaintext"`
|
||||
}
|
||||
|
||||
type extendExpiryDTO struct {
|
||||
Users []string `json:"users"` // List of user IDs to apply to.
|
||||
Days int `json:"days" example:"1"` // Number of days to add.
|
||||
Hours int `json:"hours" example:"2"` // Number of hours to add.
|
||||
Minutes int `json:"minutes" example:"3"` // Number of minutes to add.
|
||||
}
|
||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -236,9 +236,9 @@
|
||||
"integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY="
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.8.50",
|
||||
"resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.50.tgz",
|
||||
"integrity": "sha1-6/JP3gza0aNpeJ3W/XqCCwoB5Gw="
|
||||
"version": "0.8.53",
|
||||
"resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.53.tgz",
|
||||
"integrity": "sha1-tAi7DKGynasT2Lv31Z9Zr+Z3boY="
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
|
@ -19,7 +19,7 @@
|
||||
"dependencies": {
|
||||
"@ts-stack/markdown": "^1.3.0",
|
||||
"a17t": "^0.4.0",
|
||||
"esbuild": "^0.8.50",
|
||||
"esbuild": "^0.8.53",
|
||||
"lodash": "^4.17.19",
|
||||
"mjml": "^4.8.0",
|
||||
"remixicon": "^2.5.0",
|
||||
|
@ -126,6 +126,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.DELETE(p+"/users", app.DeleteUsers)
|
||||
api.GET(p+"/users", app.GetUsers)
|
||||
api.POST(p+"/users", app.NewUserAdmin)
|
||||
api.POST(p+"/users/extend", app.ExtendExpiry)
|
||||
api.POST(p+"/invites", app.GenerateInvite)
|
||||
api.GET(p+"/invites", app.GetInvites)
|
||||
api.DELETE(p+"/invites", app.DeleteInvite)
|
||||
|
34
storage.go
34
storage.go
@ -14,16 +14,17 @@ import (
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path string
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
emails, displayprefs, ombi_template map[string]interface{}
|
||||
customEmails customEmails
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path string
|
||||
users map[string]time.Time
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
emails, displayprefs, ombi_template map[string]interface{}
|
||||
customEmails customEmails
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
}
|
||||
|
||||
type customEmails struct {
|
||||
@ -34,6 +35,7 @@ type customEmails struct {
|
||||
InviteEmail customEmail `json:"inviteEmail"`
|
||||
WelcomeEmail customEmail `json:"welcomeEmail"`
|
||||
EmailConfirmation customEmail `json:"emailConfirmation"`
|
||||
UserExpired customEmail `json:"userExpired"`
|
||||
}
|
||||
|
||||
type customEmail struct {
|
||||
@ -59,6 +61,10 @@ type Invite struct {
|
||||
NoLimit bool `json:"no-limit"`
|
||||
RemainingUses int `json:"remaining-uses"`
|
||||
ValidTill time.Time `json:"valid_till"`
|
||||
UserExpiry bool `json:"user-duration"`
|
||||
UserDays int `json:"user-days,omitempty"`
|
||||
UserHours int `json:"user-hours,omitempty"`
|
||||
UserMinutes int `json:"user-minutes,omitempty"`
|
||||
Email string `json:"email"`
|
||||
UsedBy [][]string `json:"used-by"`
|
||||
Notify map[string]map[string]bool `json:"notify"`
|
||||
@ -403,6 +409,14 @@ func (st *Storage) storeInvites() error {
|
||||
return storeJSON(st.invite_path, st.invites)
|
||||
}
|
||||
|
||||
func (st *Storage) loadUsers() error {
|
||||
return loadJSON(st.users_path, &st.users)
|
||||
}
|
||||
|
||||
func (st *Storage) storeUsers() error {
|
||||
return storeJSON(st.users_path, st.users)
|
||||
}
|
||||
|
||||
func (st *Storage) loadEmails() error {
|
||||
return loadJSON(st.emails_path, &st.emails)
|
||||
}
|
||||
|
@ -57,6 +57,8 @@ window.availableProfiles = window.availableProfiles || [];
|
||||
window.modals.editor = new Modal(document.getElementById("modal-editor"));
|
||||
|
||||
window.modals.customizeEmails = new Modal(document.getElementById("modal-customize"));
|
||||
|
||||
window.modals.extendExpiry = new Modal(document.getElementById("modal-extend-expiry"));
|
||||
})();
|
||||
|
||||
var inviteCreator = new createInvite();
|
||||
|
18
ts/form.ts
18
ts/form.ts
@ -10,6 +10,11 @@ interface formWindow extends Window {
|
||||
messages: { [key: string]: string };
|
||||
confirmation: boolean;
|
||||
confirmationModal: Modal
|
||||
userExpiryEnabled: boolean;
|
||||
userExpiryDays: number;
|
||||
userExpiryHours: number;
|
||||
userExpiryMinutes: number;
|
||||
userExpiryMessage: string;
|
||||
}
|
||||
|
||||
interface pwValString {
|
||||
@ -34,6 +39,19 @@ if (window.confirmation) {
|
||||
}
|
||||
declare var window: formWindow;
|
||||
|
||||
if (window.userExpiryEnabled) {
|
||||
const messageEl = document.getElementById("user-expiry-message") as HTMLElement;
|
||||
const calculateTime = () => {
|
||||
let time = new Date()
|
||||
time.setDate(time.getDate() + window.userExpiryDays);
|
||||
time.setHours(time.getHours() + window.userExpiryHours);
|
||||
time.setMinutes(time.getMinutes() + window.userExpiryMinutes);
|
||||
messageEl.textContent = window.userExpiryMessage.replace("{date}", time.toDateString() + " " + time.toLocaleTimeString());
|
||||
setTimeout(calculateTime, 1000);
|
||||
};
|
||||
calculateTime();
|
||||
}
|
||||
|
||||
var defaultPwValStrings: pwValStrings = {
|
||||
length: {
|
||||
singular: "Must have at least {n} character",
|
||||
|
@ -6,6 +6,8 @@ interface User {
|
||||
email: string | undefined;
|
||||
last_active: string;
|
||||
admin: boolean;
|
||||
disabled: boolean;
|
||||
expiry: string;
|
||||
}
|
||||
|
||||
class user implements User {
|
||||
@ -13,9 +15,11 @@ class user implements User {
|
||||
private _check: HTMLInputElement;
|
||||
private _username: HTMLSpanElement;
|
||||
private _admin: HTMLSpanElement;
|
||||
private _disabled: HTMLSpanElement;
|
||||
private _email: HTMLInputElement;
|
||||
private _emailAddress: string;
|
||||
private _emailEditButton: HTMLElement;
|
||||
private _expiry: HTMLTableDataCellElement;
|
||||
private _lastActive: HTMLTableDataCellElement;
|
||||
id: string;
|
||||
private _selected: boolean;
|
||||
@ -34,10 +38,21 @@ class user implements User {
|
||||
set admin(state: boolean) {
|
||||
if (state) {
|
||||
this._admin.classList.add("chip", "~info", "ml-1");
|
||||
this._admin.textContent = "Admin";
|
||||
this._admin.textContent = window.lang.strings("admin");
|
||||
} else {
|
||||
this._admin.classList.remove("chip", "~info", "ml-1");
|
||||
this._admin.textContent = ""
|
||||
this._admin.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
get disabled(): boolean { return this._disabled.classList.contains("chip"); }
|
||||
set disabled(state: boolean) {
|
||||
if (state) {
|
||||
this._disabled.classList.add("chip", "~warning", "ml-1");
|
||||
this._disabled.textContent = window.lang.strings("disabled");
|
||||
} else {
|
||||
this._disabled.classList.remove("chip", "~warning", "ml-1");
|
||||
this._disabled.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +67,9 @@ class user implements User {
|
||||
}
|
||||
}
|
||||
|
||||
get expiry(): string { return this._expiry.textContent; }
|
||||
set expiry(value: string) { this._expiry.textContent = value; }
|
||||
|
||||
get last_active(): string { return this._lastActive.textContent; }
|
||||
set last_active(value: string) { this._lastActive.textContent = value; }
|
||||
|
||||
@ -62,16 +80,19 @@ class user implements User {
|
||||
this._row = document.createElement("tr") as HTMLTableRowElement;
|
||||
this._row.innerHTML = `
|
||||
<td><input type="checkbox" value=""></td>
|
||||
<td><span class="accounts-username"></span> <span class="accounts-admin"></span></td>
|
||||
<td><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></td>
|
||||
<td><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></td>
|
||||
<td class="accounts-expiry"></td>
|
||||
<td class="accounts-last-active"></td>
|
||||
`;
|
||||
const emailEditor = `<input type="email" class="input ~neutral !normal stealth-input">`;
|
||||
this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement;
|
||||
this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement;
|
||||
this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement;
|
||||
this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement;
|
||||
this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement;
|
||||
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
|
||||
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
|
||||
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
|
||||
this._check.onchange = () => { this.selected = this._check.checked; }
|
||||
|
||||
@ -130,6 +151,8 @@ class user implements User {
|
||||
this.email = user.email || "";
|
||||
this.last_active = user.last_active;
|
||||
this.admin = user.admin;
|
||||
this.disabled = user.disabled;
|
||||
this.expiry = user.expiry;
|
||||
}
|
||||
|
||||
asElement = (): HTMLTableRowElement => { return this._row; }
|
||||
@ -152,6 +175,7 @@ export class accountsList {
|
||||
private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement;
|
||||
private _deleteNotify = document.getElementById("delete-user-notify") as HTMLInputElement;
|
||||
private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement;
|
||||
private _extendExpiry = document.getElementById("accounts-extend-expiry") as HTMLSpanElement;
|
||||
private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement;
|
||||
private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement;
|
||||
private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement;
|
||||
@ -167,6 +191,24 @@ export class accountsList {
|
||||
private _addUserEmail = this._addUserForm.querySelector("input[type=email]") as HTMLInputElement;
|
||||
private _addUserPassword = this._addUserForm.querySelector("input[type=password]") as HTMLInputElement;
|
||||
|
||||
private _count = 30;
|
||||
private _populateNumbers = () => {
|
||||
const fieldIDs = ["days", "hours", "minutes"];
|
||||
const prefixes = ["extend-expiry-"];
|
||||
for (let i = 0; i < fieldIDs.length; i++) {
|
||||
for (let j = 0; j < prefixes.length; j++) {
|
||||
const field = document.getElementById(prefixes[j] + fieldIDs[i]);
|
||||
field.textContent = '';
|
||||
for (let n = 0; n <= this._count; n++) {
|
||||
const opt = document.createElement("option") as HTMLOptionElement;
|
||||
opt.textContent = ""+n;
|
||||
opt.value = ""+n;
|
||||
field.appendChild(opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get selectAll(): boolean { return this._selectAll.checked; }
|
||||
set selectAll(state: boolean) {
|
||||
for (let id in this._users) {
|
||||
@ -193,6 +235,7 @@ export class accountsList {
|
||||
if (window.emailEnabled) {
|
||||
this._announceButton.classList.add("unfocused");
|
||||
}
|
||||
this._extendExpiry.classList.add("unfocused");
|
||||
} else {
|
||||
if (this._checkCount == Object.keys(this._users).length) {
|
||||
this._selectAll.checked = true;
|
||||
@ -207,6 +250,18 @@ export class accountsList {
|
||||
if (window.emailEnabled) {
|
||||
this._announceButton.classList.remove("unfocused");
|
||||
}
|
||||
const list = this._collectUsers();
|
||||
let anyNonExpiries = false;
|
||||
for (let id of list) {
|
||||
if (!this._users[id].expiry) {
|
||||
anyNonExpiries = true;
|
||||
this._extendExpiry.classList.add("unfocused");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anyNonExpiries) {
|
||||
this._extendExpiry.classList.remove("unfocused");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,7 +449,39 @@ export class accountsList {
|
||||
window.modals.modifyUser.show();
|
||||
}
|
||||
|
||||
extendExpiry = () => {
|
||||
const list = this._collectUsers();
|
||||
let applyList: string[] = [];
|
||||
for (let id of list) {
|
||||
if (this._users[id].expiry) {
|
||||
applyList.push(id);
|
||||
}
|
||||
}
|
||||
document.getElementById("header-extend-expiry").textContent = window.lang.quantity("extendExpiry", applyList.length);
|
||||
const form = document.getElementById("form-extend-expiry") as HTMLFormElement;
|
||||
form.onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
let send = { "users": applyList }
|
||||
for (let field of ["days", "hours", "minutes"]) {
|
||||
send[field] = +(document.getElementById("extend-expiry-"+field) as HTMLSelectElement).value;
|
||||
}
|
||||
_post("/users/extend", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200 && req.status != 204) {
|
||||
window.notifications.customError("extendExpiryError", window.lang.notif("errorFailureCheckLogs"));
|
||||
} else {
|
||||
window.notifications.customSuccess("extendExpiry", window.lang.quantity("extendedExpiry", applyList.length));
|
||||
}
|
||||
window.modals.extendExpiry.close()
|
||||
this.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
window.modals.extendExpiry.show();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._populateNumbers();
|
||||
this._users = {};
|
||||
this._selectAll.checked = false;
|
||||
this._selectAll.onchange = () => { this.selectAll = this._selectAll.checked };
|
||||
@ -440,6 +527,9 @@ export class accountsList {
|
||||
this._announceButton.onclick = this.announce;
|
||||
this._announceButton.classList.add("unfocused");
|
||||
|
||||
this._extendExpiry.onclick = this.extendExpiry;
|
||||
this._extendExpiry.classList.add("unfocused");
|
||||
|
||||
if (!window.usernameEnabled) {
|
||||
this._addUserName.classList.add("unfocused");
|
||||
this._addUserName = this._addUserEmail;
|
||||
|
@ -59,7 +59,20 @@ export class DOMInvite implements Invite {
|
||||
get expiresIn(): string { return this._expiresIn }
|
||||
set expiresIn(expiry: string) {
|
||||
this._expiresIn = expiry;
|
||||
this._infoArea.querySelector("span.inv-expiry").textContent = expiry;
|
||||
this._infoArea.querySelector("span.inv-duration").textContent = expiry;
|
||||
}
|
||||
|
||||
private _userExpiry: string;
|
||||
get userExpiryTime(): string { return this._userExpiry; }
|
||||
set userExpiryTime(d: string) {
|
||||
const expiry = this._middle.querySelector("span.user-expiry") as HTMLSpanElement;
|
||||
if (!d) {
|
||||
expiry.textContent = "";
|
||||
} else {
|
||||
expiry.textContent = window.lang.strings("userExpiry");
|
||||
}
|
||||
this._userExpiry = d;
|
||||
this._middle.querySelector("strong.user-expiry-time").textContent = d;
|
||||
}
|
||||
|
||||
private _remainingUses: string = "1";
|
||||
@ -265,7 +278,7 @@ export class DOMInvite implements Invite {
|
||||
<span class="inv-email-chip"><i></i></span>
|
||||
<span class="content sm"></span>
|
||||
</div>
|
||||
<span class="inv-expiry mr-1"></span>
|
||||
<span class="inv-duration mr-1"></span>
|
||||
<span class="button ~critical !normal inv-delete">${window.lang.strings("delete")}</span>
|
||||
<label>
|
||||
<i class="icon clickable ri-arrow-down-s-line not-rotated"></i>
|
||||
@ -331,6 +344,7 @@ export class DOMInvite implements Invite {
|
||||
this._middle.innerHTML = `
|
||||
<p class="supra mb-1 top">${window.lang.strings("inviteDateCreated")} <strong class="inv-created"></strong></p>
|
||||
<p class="supra mb-1">${window.lang.strings("inviteRemainingUses")} <strong class="inv-remaining"></strong></p>
|
||||
<p class="supra mb-1"><span class="user-expiry"></span> <strong class="user-expiry-time"></strong></p>
|
||||
`;
|
||||
|
||||
this._right = document.createElement('div') as HTMLDivElement;
|
||||
@ -362,6 +376,7 @@ export class DOMInvite implements Invite {
|
||||
if (invite.label) {
|
||||
this.label = invite.label;
|
||||
}
|
||||
this.userExpiryTime = invite.userExpiryTime || "";
|
||||
}
|
||||
|
||||
asElement = (): HTMLDivElement => { return this._container; }
|
||||
@ -462,13 +477,25 @@ function parseInvite(invite: { [f: string]: string | number | string[][] | boole
|
||||
parsed.email = invite["email"] as string || "";
|
||||
parsed.label = invite["label"] as string || "";
|
||||
let time = "";
|
||||
let userExpiryTime = "";
|
||||
const fields = ["days", "hours", "minutes"];
|
||||
let prefixes = [""];
|
||||
if (invite["user-expiry"] as boolean) { prefixes.push("user-"); }
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
if (invite[fields[i]] != 0) {
|
||||
time += `${invite[fields[i]]}${fields[i][0]} `;
|
||||
for (let j = 0; j < prefixes.length; j++) {
|
||||
if (invite[prefixes[j]+fields[i]]) {
|
||||
let text = `${invite[prefixes[j]+fields[i]]}${fields[i][0]} `;
|
||||
if (prefixes[j] == "user-") {
|
||||
userExpiryTime += text;
|
||||
} else {
|
||||
time += text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parsed.expiresIn = window.lang.var("strings", "inviteExpiresInTime", time.slice(0, -1));
|
||||
parsed.userExpiry = invite["user-expiry"] as boolean;
|
||||
parsed.userExpiryTime = userExpiryTime.slice(0, -1);
|
||||
parsed.remainingUses = invite["no-limit"] ? "∞" : String(invite["remaining-uses"])
|
||||
parsed.usedBy = invite["used-by"] as string[][] || [];
|
||||
parsed.created = invite["created"] as string || window.lang.strings("unknown");
|
||||
@ -481,6 +508,7 @@ function parseInvite(invite: { [f: string]: string | number | string[][] | boole
|
||||
export class createInvite {
|
||||
private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement;
|
||||
private _sendTo = document.getElementById("create-send-to") as HTMLInputElement;
|
||||
private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement;
|
||||
private _uses = document.getElementById('create-uses') as HTMLInputElement;
|
||||
private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement;
|
||||
private _infUsesWarning = document.getElementById('create-inf-uses-warning') as HTMLParagraphElement;
|
||||
@ -491,6 +519,14 @@ export class createInvite {
|
||||
private _days = document.getElementById("create-days") as HTMLSelectElement;
|
||||
private _hours = document.getElementById("create-hours") as HTMLSelectElement;
|
||||
private _minutes = document.getElementById("create-minutes") as HTMLSelectElement;
|
||||
private _userDays = document.getElementById("user-days") as HTMLSelectElement;
|
||||
private _userHours = document.getElementById("user-hours") as HTMLSelectElement;
|
||||
private _userMinutes = document.getElementById("user-minutes") as HTMLSelectElement;
|
||||
|
||||
private _invDurationButton = document.getElementById('radio-inv-duration') as HTMLInputElement;
|
||||
private _userExpiryButton = document.getElementById('radio-user-expiry') as HTMLInputElement;
|
||||
private _invDuration = document.getElementById('inv-duration');
|
||||
private _userExpiry = document.getElementById('user-expiry');
|
||||
|
||||
// Broadcast when new invite created
|
||||
private _newInviteEvent = new CustomEvent("newInviteEvent");
|
||||
@ -498,15 +534,18 @@ export class createInvite {
|
||||
|
||||
private _count: Number = 30;
|
||||
private _populateNumbers = () => {
|
||||
const fieldIDs = ["create-days", "create-hours", "create-minutes"];
|
||||
const fieldIDs = ["days", "hours", "minutes"];
|
||||
const prefixes = ["create-", "user-"];
|
||||
for (let i = 0; i < fieldIDs.length; i++) {
|
||||
const field = document.getElementById(fieldIDs[i]);
|
||||
field.textContent = '';
|
||||
for (let n = 0; n <= this._count; n++) {
|
||||
const opt = document.createElement("option") as HTMLOptionElement;
|
||||
opt.textContent = ""+n;
|
||||
opt.value = ""+n;
|
||||
field.appendChild(opt);
|
||||
for (let j = 0; j < prefixes.length; j++) {
|
||||
const field = document.getElementById(prefixes[j] + fieldIDs[i]);
|
||||
field.textContent = '';
|
||||
for (let n = 0; n <= this._count; n++) {
|
||||
const opt = document.createElement("option") as HTMLOptionElement;
|
||||
opt.textContent = ""+n;
|
||||
opt.value = ""+n;
|
||||
field.appendChild(opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -580,6 +619,41 @@ export class createInvite {
|
||||
this._minutes.value = ""+n;
|
||||
this._checkDurationValidity();
|
||||
}
|
||||
get userExpiry(): boolean {
|
||||
return this._userExpiryToggle.checked;
|
||||
}
|
||||
set userExpiry(enabled: boolean) {
|
||||
this._userExpiryToggle.checked = enabled;
|
||||
const parent = this._userExpiryToggle.parentElement;
|
||||
if (enabled) {
|
||||
parent.classList.add("~urge");
|
||||
parent.classList.remove("~neutral");
|
||||
} else {
|
||||
parent.classList.add("~neutral");
|
||||
parent.classList.remove("~urge");
|
||||
}
|
||||
this._userDays.disabled = !enabled;
|
||||
this._userHours.disabled = !enabled;
|
||||
this._userMinutes.disabled = !enabled;
|
||||
}
|
||||
get userDays(): number {
|
||||
return +this._userDays.value;
|
||||
}
|
||||
set userDays(n: number) {
|
||||
this._userDays.value = ""+n;
|
||||
}
|
||||
get userHours(): number {
|
||||
return +this._userHours.value;
|
||||
}
|
||||
set userHours(n: number) {
|
||||
this._userHours.value = ""+n;
|
||||
}
|
||||
get userMinutes(): number {
|
||||
return +this._userMinutes.value;
|
||||
}
|
||||
set userMinutes(n: number) {
|
||||
this._userMinutes.value = ""+n;
|
||||
}
|
||||
|
||||
get sendTo(): string { return this._sendTo.value; }
|
||||
set sendTo(address: string) { this._sendTo.value = address; }
|
||||
@ -613,10 +687,18 @@ export class createInvite {
|
||||
|
||||
create = () => {
|
||||
toggleLoader(this._createButton);
|
||||
let userExpiry = this.userExpiry;
|
||||
if (this.userDays == 0 && this.userHours == 0 && this.userMinutes == 0) {
|
||||
userExpiry = false;
|
||||
}
|
||||
let send = {
|
||||
"days": this.days,
|
||||
"hours": this.hours,
|
||||
"minutes": this.minutes,
|
||||
"user-expiry": userExpiry,
|
||||
"user-days": this.userDays,
|
||||
"user-hours": this.userHours,
|
||||
"user-minutes": this.userMinutes,
|
||||
"multiple-uses": (this.uses > 1 || this.infiniteUses),
|
||||
"no-limit": this.infiniteUses,
|
||||
"remaining-uses": this.uses,
|
||||
@ -642,12 +724,42 @@ export class createInvite {
|
||||
this._infUses.onchange = () => { this.infiniteUses = this.infiniteUses; };
|
||||
this.infiniteUses = false;
|
||||
this._sendToEnabled.onchange = () => { this.sendToEnabled = this.sendToEnabled; };
|
||||
this.userExpiry = false;
|
||||
this._userExpiryToggle.onchange = () => { this.userExpiry = this._userExpiryToggle.checked; }
|
||||
this._userDays.disabled = true;
|
||||
this._userHours.disabled = true;
|
||||
this._userMinutes.disabled = true;
|
||||
this.sendToEnabled = false;
|
||||
this._createButton.onclick = this.create;
|
||||
this.sendTo = "";
|
||||
this.uses = 1;
|
||||
this.label = "";
|
||||
|
||||
const checkDuration = () => {
|
||||
const invSpan = this._invDurationButton.nextElementSibling as HTMLSpanElement;
|
||||
const userSpan = this._userExpiryButton.nextElementSibling as HTMLSpanElement;
|
||||
if (this._invDurationButton.checked) {
|
||||
this._invDuration.classList.remove("unfocused");
|
||||
this._userExpiry.classList.add("unfocused");
|
||||
invSpan.classList.add("!high");
|
||||
invSpan.classList.remove("!normal");
|
||||
userSpan.classList.add("!normal");
|
||||
userSpan.classList.remove("!high");
|
||||
} else if (this._userExpiryButton.checked) {
|
||||
this._userExpiry.classList.remove("unfocused");
|
||||
this._invDuration.classList.add("unfocused");
|
||||
invSpan.classList.add("!normal");
|
||||
invSpan.classList.remove("!high");
|
||||
userSpan.classList.add("!high");
|
||||
userSpan.classList.remove("!normal");
|
||||
}
|
||||
};
|
||||
|
||||
this._userExpiryButton.checked = false;
|
||||
this._invDurationButton.checked = true;
|
||||
this._userExpiryButton.onchange = checkDuration;
|
||||
this._invDurationButton.onchange = checkDuration;
|
||||
|
||||
this._days.onchange = this._checkDurationValidity;
|
||||
this._hours.onchange = this._checkDurationValidity;
|
||||
this._minutes.onchange = this._checkDurationValidity;
|
||||
|
@ -694,6 +694,7 @@ class EmailEditor {
|
||||
private _form = document.getElementById("form-editor") as HTMLFormElement;
|
||||
private _header = document.getElementById("header-editor") as HTMLSpanElement;
|
||||
private _variables = document.getElementById("editor-variables") as HTMLDivElement;
|
||||
private _variablesLabel = document.getElementById("label-editor-variables") as HTMLElement;
|
||||
private _textArea = document.getElementById("textarea-editor") as HTMLTextAreaElement;
|
||||
private _preview = document.getElementById("editor-preview") as HTMLDivElement;
|
||||
private _previewContent: HTMLElement;
|
||||
@ -745,6 +746,11 @@ class EmailEditor {
|
||||
let ci = i % colors.length;
|
||||
innerHTML += '<span class="button ~' + colors[ci] +' !normal mb-1" style="margin-left: 0.25rem; margin-right: 0.25rem;"></span>'
|
||||
}
|
||||
if (this._templ.variables.length == 0) {
|
||||
this._variablesLabel.classList.add("unfocused");
|
||||
} else {
|
||||
this._variablesLabel.classList.remove("unfocused");
|
||||
}
|
||||
this._variables.innerHTML = innerHTML
|
||||
const buttons = this._variables.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
|
||||
for (let i = 0; i < this._templ.variables.length; i++) {
|
||||
@ -761,10 +767,12 @@ class EmailEditor {
|
||||
}
|
||||
loadPreview = () => {
|
||||
let content = this._textArea.value;
|
||||
for (let variable of this._templ.variables) {
|
||||
let value = this._templ.values[variable.slice(1, -1)];
|
||||
if (value === undefined) { value = variable; }
|
||||
content = content.replace(new RegExp(variable, "g"), value);
|
||||
if (this._templ.variables) {
|
||||
for (let variable of this._templ.variables) {
|
||||
let value = this._templ.values[variable.slice(1, -1)];
|
||||
if (value === undefined) { value = variable; }
|
||||
content = content.replace(new RegExp(variable, "g"), value);
|
||||
}
|
||||
}
|
||||
if (this._templ.html == "") {
|
||||
content = stripMarkdown(content);
|
||||
@ -785,7 +793,7 @@ class EmailEditor {
|
||||
}
|
||||
|
||||
showList = () => {
|
||||
_get("/config/emails", null, (req: XMLHttpRequest) => {
|
||||
_get("/config/emails?lang=" + window.language, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs"));
|
||||
|
@ -77,6 +77,7 @@ declare interface Modals {
|
||||
announce: Modal;
|
||||
editor: Modal;
|
||||
customizeEmails: Modal;
|
||||
extendExpiry: Modal;
|
||||
}
|
||||
|
||||
interface Invite {
|
||||
@ -90,6 +91,8 @@ interface Invite {
|
||||
notifyCreation?: boolean;
|
||||
profile?: string;
|
||||
label?: string;
|
||||
userExpiry?: boolean;
|
||||
userExpiryTime?: string;
|
||||
}
|
||||
|
||||
interface inviteList {
|
||||
|
113
userdaemon.go
Normal file
113
userdaemon.go
Normal file
@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/mediabrowser"
|
||||
)
|
||||
|
||||
type userDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
Interval time.Duration
|
||||
period time.Duration
|
||||
app *appContext
|
||||
}
|
||||
|
||||
func newUserDaemon(interval time.Duration, app *appContext) *userDaemon {
|
||||
return &userDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
Interval: interval,
|
||||
period: interval,
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *userDaemon) run() {
|
||||
rt.app.info.Println("User daemon started")
|
||||
for {
|
||||
select {
|
||||
case <-rt.ShutdownChannel:
|
||||
rt.ShutdownChannel <- "Down"
|
||||
return
|
||||
case <-time.After(rt.period):
|
||||
break
|
||||
}
|
||||
started := time.Now()
|
||||
rt.app.storage.loadInvites()
|
||||
rt.app.checkUsers()
|
||||
finished := time.Now()
|
||||
duration := finished.Sub(started)
|
||||
rt.period = rt.Interval - duration
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) checkUsers() {
|
||||
if len(app.storage.users) == 0 {
|
||||
return
|
||||
}
|
||||
app.info.Println("Daemon: Checking for user expiry")
|
||||
users, status, err := app.jf.GetUsers(false)
|
||||
if err != nil || status != 200 {
|
||||
app.err.Printf("Failed to get users (%d): %s", status, err)
|
||||
return
|
||||
}
|
||||
mode := "disable"
|
||||
termPlural := "Disabling"
|
||||
if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" {
|
||||
mode = "delete"
|
||||
termPlural = "Deleting"
|
||||
}
|
||||
email := false
|
||||
if emailEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
|
||||
email = true
|
||||
}
|
||||
for id, expiry := range app.storage.users {
|
||||
if time.Now().After(expiry) {
|
||||
found := false
|
||||
var user mediabrowser.User
|
||||
for _, u := range users {
|
||||
if u.ID == id {
|
||||
found = true
|
||||
user = u
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
app.info.Printf("Expired user already deleted, ignoring.")
|
||||
continue
|
||||
}
|
||||
app.info.Printf("%s expired user \"%s\"", termPlural, user.Name)
|
||||
if mode == "delete" {
|
||||
status, err = app.jf.DeleteUser(id)
|
||||
} else if mode == "disable" {
|
||||
user.Policy.IsDisabled = true
|
||||
status, err = app.jf.SetPolicy(id, user.Policy)
|
||||
}
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to %s \"%s\" (%d): %s", mode, user.Name, status, err)
|
||||
continue
|
||||
}
|
||||
delete(app.storage.users, id)
|
||||
if email {
|
||||
address, ok := app.storage.emails[id]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
msg, err := app.email.constructUserExpired(app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct expiry email for \"%s\": %s", user.Name, err)
|
||||
} else if err := app.email.send(msg, address.(string)); err != nil {
|
||||
app.err.Printf("Failed to send expiry email to \"%s\": %s", user.Name, err)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to \"%s\"", address.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
err = app.storage.storeUsers()
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to store user duration: %s", err)
|
||||
}
|
||||
}
|
6
views.go
6
views.go
@ -81,6 +81,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
"strings": app.storage.lang.Admin[lang].Strings,
|
||||
"quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
|
||||
"language": app.storage.lang.Admin[lang].JSON,
|
||||
"langName": lang,
|
||||
})
|
||||
}
|
||||
|
||||
@ -182,6 +183,11 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
"notifications": app.storage.lang.Form[lang].notificationsJSON,
|
||||
"code": code,
|
||||
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
|
||||
"userExpiry": inv.UserExpiry,
|
||||
"userExpiryDays": inv.UserDays,
|
||||
"userExpiryHours": inv.UserHours,
|
||||
"userExpiryMinutes": inv.UserMinutes,
|
||||
"userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"),
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user