1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 09:00:10 +00:00

accounts: add user enable/disable & emails

This commit is contained in:
Harvey Tindall 2021-04-12 21:28:36 +01:00
parent dafb439a7d
commit 55e21f8be3
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
23 changed files with 437 additions and 58 deletions

168
api.go
View File

@ -573,6 +573,70 @@ func (app *appContext) Announce(gc *gin.Context) {
respondBool(200, true, gc)
}
// @Summary Enable/Disable a list of users, optionally notifying them why.
// @Produce json
// @Param enableDisableUserDTO body enableDisableUserDTO true "User enable/disable request object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 500 {object} errorListDTO "List of errors"
// @Router /users/enable [post]
// @Security Bearer
// @tags Users
func (app *appContext) EnableDisableUsers(gc *gin.Context) {
var req enableDisableUserDTO
gc.BindJSON(&req)
errors := errorListDTO{
"GetUser": map[string]string{},
"SetPolicy": map[string]string{},
}
var addresses []string
for _, userID := range req.Users {
user, status, err := app.jf.UserByID(userID, false)
if status != 200 || err != nil {
errors["GetUser"][userID] = fmt.Sprintf("%d %v", status, err)
app.err.Printf("Failed to get user \"%s\" (%d): %v", userID, status, err)
continue
}
user.Policy.IsDisabled = !req.Enabled
status, err = app.jf.SetPolicy(userID, user.Policy)
if !(status == 200 || status == 204) || err != nil {
errors["SetPolicy"][userID] = fmt.Sprintf("%d %v", status, err)
app.err.Printf("Failed to set policy for user \"%s\" (%d): %v", userID, status, err)
continue
}
if emailEnabled && req.Notify {
addr, ok := app.storage.emails[userID]
if addr != nil && ok {
addresses = append(addresses, addr.(string))
}
}
}
if len(addresses) != 0 {
go func(reason string, addresses []string) {
var msg *Email
var err error
if req.Enabled {
msg, err = app.email.constructEnabled(reason, app, false)
} else {
msg, err = app.email.constructDisabled(reason, app, false)
}
if err != nil {
app.err.Printf("Failed to construct account enabled/disabled emails: %v", err)
} else if err := app.email.send(msg, addresses...); err != nil {
app.err.Printf("Failed to send account enabled/disabled emails: %v", err)
} else {
app.info.Println("Sent account enabled/disabled emails")
}
}(req.Reason, addresses)
}
app.jf.CacheExpiry = time.Now()
if len(errors["GetUser"]) != 0 || len(errors["SetPolicy"]) != 0 {
gc.JSON(500, errors)
return
}
respondBool(200, true, gc)
}
// @Summary Delete a list of users, optionally notifying them why.
// @Produce json
// @Param deleteUserDTO body deleteUserDTO true "User deletion request object"
@ -1379,6 +1443,8 @@ func (app *appContext) GetEmails(gc *gin.Context) {
"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},
"UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.customEmails.UserDisabled.Enabled},
"UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.customEmails.UserEnabled.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},
@ -1414,6 +1480,12 @@ func (app *appContext) SetEmail(gc *gin.Context) {
} else if id == "UserDeleted" {
app.storage.customEmails.UserDeleted.Content = req.Content
app.storage.customEmails.UserDeleted.Enabled = true
} else if id == "UserDisabled" {
app.storage.customEmails.UserDisabled.Content = req.Content
app.storage.customEmails.UserDisabled.Enabled = true
} else if id == "UserEnabled" {
app.storage.customEmails.UserEnabled.Content = req.Content
app.storage.customEmails.UserEnabled.Enabled = true
} else if id == "InviteEmail" {
app.storage.customEmails.InviteEmail.Content = req.Content
app.storage.customEmails.InviteEmail.Enabled = true
@ -1461,6 +1533,10 @@ func (app *appContext) SetEmailState(gc *gin.Context) {
app.storage.customEmails.PasswordReset.Enabled = enabled
} else if id == "UserDeleted" {
app.storage.customEmails.UserDeleted.Enabled = enabled
} else if id == "UserDisabled" {
app.storage.customEmails.UserDisabled.Enabled = enabled
} else if id == "UserEnabled" {
app.storage.customEmails.UserEnabled.Enabled = enabled
} else if id == "InviteEmail" {
app.storage.customEmails.InviteEmail.Enabled = enabled
} else if id == "WelcomeEmail" {
@ -1480,39 +1556,6 @@ func (app *appContext) SetEmailState(gc *gin.Context) {
respondBool(200, true, gc)
}
// @Summary Returns whether there's a new update, and extra info if there is.
// @Produce json
// @Success 200 {object} checkUpdateDTO
// @Router /config/update [get]
// @tags Configuration
func (app *appContext) CheckUpdate(gc *gin.Context) {
if !app.newUpdate {
app.update = Update{}
}
gc.JSON(200, checkUpdateDTO{New: app.newUpdate, Update: app.update})
}
// @Summary Apply an update.
// @Produce json
// @Success 200 {object} boolResponse
// @Success 400 {object} stringResponse
// @Success 500 {object} boolResponse
// @Router /config/update [post]
// @tags Configuration
func (app *appContext) ApplyUpdate(gc *gin.Context) {
if !app.update.CanUpdate {
respond(400, "Update is manual", gc)
return
}
err := app.update.update()
if err != nil {
app.err.Printf("Failed to apply update: %v", err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Returns the custom email (generating it if not set) and list of used variables in it.
// @Produce json
// @Success 200 {object} customEmailDTO
@ -1578,8 +1621,32 @@ 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[lang].UserDeleted.get("reason"), app, false)
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
// app.storage.customEmails.UserDeleted = content
} else if id == "UserDisabled" {
content = app.storage.customEmails.UserDisabled.Content
if content == "" {
newEmail = true
msg, err = app.email.constructDisabled("", app, true)
content = msg.Text
} else {
variables = app.storage.customEmails.UserDisabled.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.UserDisabled.Variables = variables }
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
// app.storage.customEmails.UserDeleted = content
} else if id == "UserEnabled" {
content = app.storage.customEmails.UserEnabled.Content
if content == "" {
newEmail = true
msg, err = app.email.constructEnabled("", app, true)
content = msg.Text
} else {
variables = app.storage.customEmails.UserEnabled.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.UserEnabled.Variables = variables }
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
// app.storage.customEmails.UserEnabled = content
} else if id == "InviteEmail" {
content = app.storage.customEmails.InviteEmail.Content
if content == "" {
@ -1670,6 +1737,39 @@ func (app *appContext) GetEmail(gc *gin.Context) {
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Values: values, HTML: email.HTML, Plaintext: email.Text})
}
// @Summary Returns whether there's a new update, and extra info if there is.
// @Produce json
// @Success 200 {object} checkUpdateDTO
// @Router /config/update [get]
// @tags Configuration
func (app *appContext) CheckUpdate(gc *gin.Context) {
if !app.newUpdate {
app.update = Update{}
}
gc.JSON(200, checkUpdateDTO{New: app.newUpdate, Update: app.update})
}
// @Summary Apply an update.
// @Produce json
// @Success 200 {object} boolResponse
// @Success 400 {object} stringResponse
// @Success 500 {object} boolResponse
// @Router /config/update [post]
// @tags Configuration
func (app *appContext) ApplyUpdate(gc *gin.Context) {
if !app.update.CanUpdate {
respond(400, "Update is manual", gc)
return
}
err := app.update.update()
if err != nil {
app.err.Printf("Failed to apply update: %v", err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Logout by deleting refresh token from cookies.
// @Produce json
// @Success 200 {object} boolResponse

View File

@ -64,6 +64,12 @@ func (app *appContext) loadConfig() error {
app.MustSetValue("deletion", "email_html", "jfa-go:"+"deleted.html")
app.MustSetValue("deletion", "email_text", "jfa-go:"+"deleted.txt")
// Deletion template is good enough for these as well.
app.MustSetValue("disable_enable", "disabled_html", "jfa-go:"+"deleted.html")
app.MustSetValue("disable_enable", "disabled_text", "jfa-go:"+"deleted.txt")
app.MustSetValue("disable_enable", "enabled_html", "jfa-go:"+"deleted.html")
app.MustSetValue("disable_enable", "enabled_text", "jfa-go:"+"deleted.txt")
app.MustSetValue("welcome_email", "email_html", "jfa-go:"+"welcome.html")
app.MustSetValue("welcome_email", "email_text", "jfa-go:"+"welcome.txt")

View File

@ -899,6 +899,68 @@
}
}
},
"disable_enable": {
"order": [],
"meta": {
"name": "Account Disabling/Enabling",
"description": "Subject/email files for account disabling/enabling emails.",
"depends_true": "email|method"
},
"settings": {
"subject_disabled": {
"name": "Email subject (Disabled)",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "Subject of account disabling emails."
},
"subject_enabled": {
"name": "Email subject (Enabled)",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "Subject of account enabling emails."
},
"disabled_html": {
"name": "Custom disabling email (HTML)",
"required": false,
"requires_restart": false,
"advanced": true,
"type": "text",
"value": "",
"description": "Path to custom email html"
},
"disabled_text": {
"name": "Custom disabling email (plaintext)",
"required": false,
"requires_restart": false,
"advanced": true,
"type": "text",
"value": "",
"description": "Path to custom email in plain text"
},
"enabled_html": {
"name": "Custom enabling email (HTML)",
"required": false,
"requires_restart": false,
"advanced": true,
"type": "text",
"value": "",
"description": "Path to custom email html"
},
"enabled_text": {
"name": "Custom enabling email (plaintext)",
"required": false,
"requires_restart": false,
"advanced": true,
"type": "text",
"value": "",
"description": "Path to custom email in plain text"
}
}
},
"deletion": {
"order": [],
"meta": {

View File

@ -535,9 +535,9 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
func (emailer *Emailer) deletedValues(reason string, app *appContext, noSub bool) map[string]interface{} {
template := map[string]interface{}{
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reasonString": emailer.lang.UserDeleted.get("reason"),
"message": "",
"yourAccountWas": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reasonString": emailer.lang.Strings.get("reason"),
"message": "",
}
if noSub {
empty := []string{"reason"}
@ -575,6 +575,90 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
return email, nil
}
func (emailer *Emailer) disabledValues(reason string, app *appContext, noSub bool) map[string]interface{} {
template := map[string]interface{}{
"yourAccountWas": emailer.lang.UserDisabled.get("yourAccountWasDisabled"),
"reasonString": emailer.lang.Strings.get("reason"),
"message": "",
}
if noSub {
empty := []string{"reason"}
for _, v := range empty {
template[v] = "{" + v + "}"
}
} else {
template["reason"] = reason
template["message"] = app.config.Section("email").Key("message").String()
}
return template
}
func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("disable_enable").Key("subject_disabled").MustString(emailer.lang.UserDisabled.get("title")),
}
var err error
template := emailer.disabledValues(reason, app, noSub)
if app.storage.customEmails.UserDisabled.Enabled {
content := app.storage.customEmails.UserDisabled.Content
for _, v := range app.storage.customEmails.UserDisabled.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, "disable_enable", "disabled_", template)
}
if err != nil {
return nil, err
}
return email, nil
}
func (emailer *Emailer) enabledValues(reason string, app *appContext, noSub bool) map[string]interface{} {
template := map[string]interface{}{
"yourAccountWas": emailer.lang.UserEnabled.get("yourAccountWasEnabled"),
"reasonString": emailer.lang.Strings.get("reason"),
"message": "",
}
if noSub {
empty := []string{"reason"}
for _, v := range empty {
template[v] = "{" + v + "}"
}
} else {
template["reason"] = reason
template["message"] = app.config.Section("email").Key("message").String()
}
return template
}
func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("disable_enable").Key("subject_enabled").MustString(emailer.lang.UserEnabled.get("title")),
}
var err error
template := emailer.enabledValues(reason, app, noSub)
if app.storage.customEmails.UserEnabled.Enabled {
content := app.storage.customEmails.UserEnabled.Content
for _, v := range app.storage.customEmails.UserEnabled.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, "disable_enable", "enabled_", template)
}
if err != nil {
return nil, err
}
return email, nil
}
func (emailer *Emailer) welcomeValues(username string, expiry time.Time, app *appContext, noSub bool, custom bool) map[string]interface{} {
template := map[string]interface{}{
"welcome": emailer.lang.WelcomeEmail.get("welcome"),

View File

@ -471,6 +471,7 @@
<span class="col sm button ~info !normal center mb-half" id="accounts-announce">{{ .strings.announce }}</span>
<span class="col sm button ~urge !normal center mb-half" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
<span class="col sm button ~warning !normal center mb-half" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
<span class="col sm button ~positive !normal center mb-half" id="accounts-disable-enable">{{ .strings.disable }}</span>
<span class="col sm button ~critical !normal center mb-half" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
</div>
</div>

View File

@ -93,6 +93,8 @@ type emailLang struct {
InviteExpiry langSection `json:"inviteExpiry"`
PasswordReset langSection `json:"passwordReset"`
UserDeleted langSection `json:"userDeleted"`
UserDisabled langSection `json:"userDisabled"`
UserEnabled langSection `json:"userEnabled"`
InviteEmail langSection `json:"inviteEmail"`
WelcomeEmail langSection `json:"welcomeEmail"`
EmailConfirmation langSection `json:"emailConfirmation"`

View File

@ -24,6 +24,8 @@
"date": "Date",
"enabled": "Enabled",
"disabled": "Disabled",
"reEnable": "Re-enable",
"disable": "Disable",
"admin": "Admin",
"updates": "Updates",
"update": "Update",
@ -136,6 +138,14 @@
"singular": "Delete {n} user",
"plural": "Delete {n} users"
},
"disableUsers": {
"singular": "Disable {n} user",
"plural": "Disable {n} users"
},
"reEnableUsers": {
"singular": "Re-enable {n} user",
"plural": "Re-enable {n} users"
},
"addUser": {
"singular": "Add user",
"plural": "Add users"
@ -148,6 +158,14 @@
"singular": "Deleted {n} user.",
"plural": "Deleted {n} users."
},
"disabledUser": {
"singular": "Disabled {n} user.",
"plural": "Disabled {n} users."
},
"enabledUser": {
"singular": "Enabled {n} user.",
"plural": "Enabled {n} users."
},
"announceTo": {
"singular": "Announce to {n} user",
"plural": "Announce to {n} users"

View File

@ -4,6 +4,7 @@
},
"strings": {
"ifItWasNotYou": "Wenn du das nicht warst, ignoriere bitte diese E-Mail.",
"reason": "Grund",
"helloUser": "Hallo {username},"
},
"userCreated": {
@ -31,7 +32,6 @@
"userDeleted": {
"title": "Dein Konto wurde gelöscht - Jellyfin",
"yourAccountWasDeleted": "Dein Jellyfin-Konto wurde gelöscht.",
"reason": "Grund",
"name": "Benutzerlöschung"
},
"inviteEmail": {

View File

@ -4,6 +4,7 @@
},
"strings": {
"ifItWasNotYou": "Αν δεν ήσασταν εσείς, παρακαλώ αγνοήστε αυτό το email.",
"reason": "Λόγος",
"helloUser": "Γεία σου {username},"
},
"userCreated": {
@ -32,7 +33,6 @@
"userDeleted": {
"title": "Ο λογαριασμός σας διαγράφηκε - Jellyfin",
"yourAccountWasDeleted": "Ο λογαριασμός σας Jellyfin διαγράφηκε.",
"reason": "Λόγος",
"name": "Διαγραφή χρήστη"
},
"inviteEmail": {

View File

@ -4,7 +4,8 @@
},
"strings": {
"ifItWasNotYou": "If this wasn't you, please ignore this email.",
"helloUser": "Hi {username},"
"helloUser": "Hi {username},",
"reason": "Reason"
},
"userCreated": {
"name": "User creation",
@ -32,8 +33,17 @@
"userDeleted": {
"name": "User deletion",
"title": "Your account was deleted - Jellyfin",
"yourAccountWasDeleted": "Your Jellyfin account was deleted.",
"reason": "Reason"
"yourAccountWasDeleted": "Your Jellyfin account was deleted."
},
"userDisabled": {
"name": "User disabled",
"title": "Your account has been disabled - Jellyfin",
"yourAccountWasDisabled": "Your account was disabled."
},
"userEnabled": {
"name": "User enabled",
"title": "Your account has been re-enabled - Jellyfin",
"yourAccountWasEnabled": "Your account was re-enabled."
},
"inviteEmail": {
"name": "Invite email",

View File

@ -5,6 +5,7 @@
},
"strings": {
"ifItWasNotYou": "Si ce n'était pas toi, tu peux ignorer ce mail.",
"reason": "Motif",
"helloUser": "Salut {username},"
},
"userCreated": {
@ -32,7 +33,6 @@
"userDeleted": {
"title": "Ton compte a été désactivé - Jellyfin",
"yourAccountWasDeleted": "Ton compte Jellyfin a été supprimé.",
"reason": "Motif",
"name": "Suppression de l'utilisateur"
},
"inviteEmail": {

View File

@ -4,6 +4,7 @@
},
"strings": {
"ifItWasNotYou": "Jika ini bukan kamu, silahkan mengabaikan email ini.",
"reason": "Alasan",
"helloUser": "Halo {username},"
},
"userCreated": {
@ -31,7 +32,6 @@
"userDeleted": {
"title": "Akun anda telah dihapus - Jellyfin",
"yourAccountWasDeleted": "Akun Jellyfin anda telah dihapus.",
"reason": "Alasan",
"name": "Penghapusan pengguna"
},
"inviteEmail": {

View File

@ -4,7 +4,8 @@
},
"strings": {
"ifItWasNotYou": "Se non sei stato tu, puoi ignorare questa email.",
"helloUser": "Ciao {username},"
"helloUser": "Ciao {username},",
"reason": "Motivo"
},
"userCreated": {
"title": "Nota: Utente creato",
@ -27,8 +28,7 @@
},
"userDeleted": {
"title": "Il tuo account è stato eliminato - Jellyfin",
"yourAccountWasDeleted": "Il tuo account di Jellyfin è stato eliminato.",
"reason": "Motivo"
"yourAccountWasDeleted": "Il tuo account di Jellyfin è stato eliminato."
},
"inviteEmail": {
"title": "Invita - Jellyfin",

View File

@ -4,6 +4,7 @@
},
"strings": {
"ifItWasNotYou": "Als jij dit niet was, negeer dan alsjeblieft deze email.",
"reason": "Reden",
"helloUser": "Hoi {username},"
},
"userCreated": {
@ -32,7 +33,6 @@
"userDeleted": {
"title": "Je account is verwijderd - Jellyfin",
"yourAccountWasDeleted": "Je Jellyfin account is verwijderd.",
"reason": "Reden",
"name": "Gebruiker verwijderd"
},
"inviteEmail": {

View File

@ -4,6 +4,7 @@
},
"strings": {
"ifItWasNotYou": "Se não foi você, ignore este e-mail.",
"reason": "Razão",
"helloUser": "Ola {username},"
},
"userCreated": {
@ -32,7 +33,6 @@
"userDeleted": {
"title": "Sua conta foi excluída - Jellyfin",
"yourAccountWasDeleted": "Sua conta Jellyfin foi excluída.",
"reason": "Razão",
"name": "Exclusão do usuário"
},
"inviteEmail": {

View File

@ -4,7 +4,8 @@
},
"strings": {
"ifItWasNotYou": "Om detta inte var du, ignorera det här e-postmeddelandet.",
"helloUser": "Hej {användarnamn},"
"helloUser": "Hej {username},",
"reason": "Anledning"
},
"userCreated": {
"name": "Användarskapande",
@ -31,8 +32,7 @@
"userDeleted": {
"name": "Radering av användare",
"title": "Ditt konto raderades - Jellyfin",
"yourAccountWasDeleted": "Ditt Jellyfin-konto raderades.",
"reason": "Anledning"
"yourAccountWasDeleted": "Ditt Jellyfin-konto raderades."
},
"inviteEmail": {
"name": "Inbjudnings e-post",

View File

@ -60,7 +60,7 @@
<mj-section mj-class="bg">
<mj-column>
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
<h3>{{ .yourAccountWasDeleted }}</h3>
<h3>{{ .yourAccountWas }}</h3>
<p>{{ .reasonString }}: <i>{{ .reason }}</i></p>
</mj-text>
</mj-column>

View File

@ -1,4 +1,4 @@
{{ .yourAccountWasDeleted }}
{{ .yourAccountWas }}
{{ .reasonString }}: {{ .reason }}

View File

@ -29,6 +29,13 @@ type deleteUserDTO struct {
Reason string `json:"reason"` // Account deletion reason (for notification)
}
type enableDisableUserDTO struct {
Users []string `json:"users" binding:"required"` // List of usernames to delete
Enabled bool `json:"enabled"` // True = enable users, False = disable.
Notify bool `json:"notify"` // Whether to notify users of deletion
Reason string `json:"reason"` // Account deletion reason (for notification)
}
type generateInviteDTO struct {
Months int `json:"months" example:"0"` // Number of months
Days int `json:"days" example:"1"` // Number of days

View File

@ -132,6 +132,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/users", app.GetUsers)
api.POST(p+"/users", app.NewUserAdmin)
api.POST(p+"/users/extend", app.ExtendExpiry)
api.POST(p+"/users/enable", app.EnableDisableUsers)
api.POST(p+"/invites", app.GenerateInvite)
api.GET(p+"/invites", app.GetInvites)
api.DELETE(p+"/invites", app.DeleteInvite)

View File

@ -34,6 +34,8 @@ type customEmails struct {
InviteExpiry customEmail `json:"inviteExpiry"`
PasswordReset customEmail `json:"passwordReset"`
UserDeleted customEmail `json:"userDeleted"`
UserDisabled customEmail `json:"userDisabled"`
UserEnabled customEmail `json:"userEnabled"`
InviteEmail customEmail `json:"inviteEmail"`
WelcomeEmail customEmail `json:"welcomeEmail"`
EmailConfirmation customEmail `json:"emailConfirmation"`
@ -431,10 +433,13 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
patchLang(&english.InviteExpiry, &lang.InviteExpiry)
patchLang(&english.PasswordReset, &lang.PasswordReset)
patchLang(&english.UserDeleted, &lang.UserDeleted)
patchLang(&english.UserDisabled, &lang.UserDisabled)
patchLang(&english.UserEnabled, &lang.UserEnabled)
patchLang(&english.InviteEmail, &lang.InviteEmail)
patchLang(&english.WelcomeEmail, &lang.WelcomeEmail)
patchLang(&english.EmailConfirmation, &lang.EmailConfirmation)
patchLang(&english.UserExpired, &lang.UserExpired)
patchLang(&english.Strings, &lang.Strings)
}
st.lang.Email[index] = lang
return nil

View File

@ -194,6 +194,7 @@ export class accountsList {
private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement;
private _announceButton = document.getElementById("accounts-announce") as HTMLSpanElement;
private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement;
private _disableEnable = document.getElementById("accounts-disable-enable") 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;
@ -209,6 +210,8 @@ export class accountsList {
private _sortedByName: string[] = [];
private _checkCount: number = 0;
private _inSearch = false;
// Whether the enable/disable button should enable or not.
private _shouldEnable = false;
private _addUserForm = document.getElementById("form-add-user") as HTMLFormElement;
private _addUserName = this._addUserForm.querySelector("input[type=text]") as HTMLInputElement;
@ -329,6 +332,7 @@ export class accountsList {
this._announceButton.classList.add("unfocused");
}
this._extendExpiry.classList.add("unfocused");
this._disableEnable.classList.add("unfocused");
} else {
let visibleCount = 0;
for (let id in this._users) {
@ -350,16 +354,37 @@ export class accountsList {
this._announceButton.classList.remove("unfocused");
}
let anyNonExpiries = list.length == 0 ? true : false;
// Only show enable/disable button if all selected have the same state.
this._shouldEnable = this._users[list[0]].disabled
let showDisableEnable = true;
for (let id of list) {
if (!this._users[id].expiry) {
if (!anyNonExpiries && !this._users[id].expiry) {
anyNonExpiries = true;
this._extendExpiry.classList.add("unfocused");
break;
}
if (showDisableEnable && this._users[id].disabled != this._shouldEnable) {
showDisableEnable = false;
this._disableEnable.classList.add("unfocused");
}
if (!showDisableEnable && anyNonExpiries) { break; }
}
if (!anyNonExpiries) {
this._extendExpiry.classList.remove("unfocused");
}
if (showDisableEnable) {
let message: string;
if (this._shouldEnable) {
message = window.lang.strings("reEnable");
this._disableEnable.classList.add("~positive");
this._disableEnable.classList.remove("~warning");
} else {
message = window.lang.strings("disable");
this._disableEnable.classList.add("~warning");
this._disableEnable.classList.remove("~positive");
}
this._disableEnable.classList.remove("unfocused");
this._disableEnable.textContent = message;
}
}
}
@ -440,13 +465,67 @@ export class accountsList {
};
window.modals.announce.show();
}
enableDisableUsers = () => {
// We can share the delete modal for this
const modalHeader = document.getElementById("header-delete-user");
const form = document.getElementById("form-delete-user") as HTMLFormElement;
const button = form.querySelector("span.submit") as HTMLSpanElement;
let list = this._collectUsers();
if (this._shouldEnable) {
modalHeader.textContent = window.lang.quantity("reEnableUsers", list.length);
button.textContent = window.lang.strings("reEnable");
button.classList.add("~urge");
button.classList.remove("~critical");
} else {
modalHeader.textContent = window.lang.quantity("disableUsers", list.length);
button.textContent = window.lang.strings("disable");
button.classList.add("~critical");
button.classList.remove("~urge");
}
this._deleteNotify.checked = false;
this._deleteReason.value = "";
this._deleteReason.classList.add("unfocused");
form.onsubmit = (event: Event) => {
event.preventDefault();
toggleLoader(button);
let send = {
"users": list,
"enabled": this._shouldEnable,
"notify": this._deleteNotify.checked,
"reason": this._deleteNotify ? this._deleteReason.value : ""
};
_post("/users/enable", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
window.modals.deleteUser.close();
if (req.status != 200 && req.status != 204) {
let errorMsg = window.lang.notif("errorFailureCheckLogs");
if (!("error" in req.response)) {
errorMsg = window.lang.notif("errorPartialFailureCheckLogs");
}
window.notifications.customError("deleteUserError", errorMsg);
} else if (this._shouldEnable) {
window.notifications.customSuccess("enableUserSuccess", window.lang.quantity("enabledUser", list.length));
} else {
window.notifications.customSuccess("disableUserSuccess", window.lang.quantity("disabledUser", list.length));
}
this.reload();
}
}, true);
}
window.modals.deleteUser.show();
}
deleteUsers = () => {
const modalHeader = document.getElementById("header-delete-user");
modalHeader.textContent = window.lang.quantity("deleteNUsers", this._collectUsers().length);
let list = this._collectUsers();
modalHeader.textContent = window.lang.quantity("deleteNUsers", list.length);
const form = document.getElementById("form-delete-user") as HTMLFormElement;
const button = form.querySelector("span.submit") as HTMLSpanElement;
button.textContent = window.lang.strings("delete");
button.classList.add("~critical");
button.classList.remove("~urge");
this._deleteNotify.checked = false;
this._deleteReason.value = "";
this._deleteReason.classList.add("unfocused");
@ -469,7 +548,7 @@ export class accountsList {
}
window.notifications.customError("deleteUserError", errorMsg);
} else {
window.notifications.customSuccess("deleteUserSuccess", window.lang.quantity("deletedUser", this._collectUsers().length));
window.notifications.customSuccess("deleteUserSuccess", window.lang.quantity("deletedUser", list.length));
}
this.reload();
}
@ -630,6 +709,9 @@ export class accountsList {
this._extendExpiry.onclick = this.extendExpiry;
this._extendExpiry.classList.add("unfocused");
this._disableEnable.onclick = this.enableDisableUsers;
this._disableEnable.classList.add("unfocused");
if (!window.usernameEnabled) {
this._addUserName.classList.add("unfocused");
this._addUserName = this._addUserEmail;

View File

@ -793,6 +793,7 @@ export class createInvite {
this._invDurationButton.onchange = checkDuration;
this._days.onchange = this._checkDurationValidity;
this._months.onchange = this._checkDurationValidity;
this._hours.onchange = this._checkDurationValidity;
this._minutes.onchange = this._checkDurationValidity;
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);