From 86c37fb4237bc645e5fcb4a52f5908ae23a9f154 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 5 Aug 2024 13:56:34 +0100 Subject: [PATCH] expiry: add option to delete after n days Much like activities, you can have an expired account disabled, then deleted after n days (unless the admin intervenes and re-enables the account). For #341. --- config/config-base.json | 9 +++ storage.go | 5 +- user-d.go | 135 +++++++++++++++++++++------------------- 3 files changed, 83 insertions(+), 66 deletions(-) diff --git a/config/config-base.json b/config/config-base.json index 08b0aca..bdeea8d 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -1797,6 +1797,15 @@ "value": "disable_user", "description": "Whether to delete or disable users on expiry." }, + "delete_expired_after_days": { + "name": "Delete expired accounts after (days)", + "required": false, + "requires_restart": false, + "type": "number", + "value": 0, + "depends_true": "behaviour", + "description": "When set, user accounts will be deleted this many days after expiring (if \"Behaviour\" is \"Disable user\"). Set to 0 to disable." + }, "send_email": { "name": "Send email", "required": false, diff --git a/storage.go b/storage.go index dbe8cba..6acc6e0 100644 --- a/storage.go +++ b/storage.go @@ -63,8 +63,9 @@ type Activity struct { } type UserExpiry struct { - JellyfinID string `badgerhold:"key"` - Expiry time.Time + JellyfinID string `badgerhold:"key"` + Expiry time.Time + DeleteAfterPeriod bool // Whether or not to further disable the user later on } type DebugLogAction int diff --git a/user-d.go b/user-d.go index 11a4012..e0a76bb 100644 --- a/user-d.go +++ b/user-d.go @@ -22,6 +22,7 @@ func (app *appContext) checkUsers() { if len(app.storage.GetUserExpiries()) == 0 { return } + app.info.Println(lm.CheckUserExpiries) users, status, err := app.jf.GetUsers(false) if err != nil || status != 200 { @@ -29,87 +30,93 @@ func (app *appContext) checkUsers() { return } mode := "disable" - phrase := lm.DisableExpiredUser if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" { mode = "delete" - phrase = lm.DeleteExpiredUser } + + deleteAfterPeriod := app.config.Section("user_expiry").Key("delete_expired_after_days").MustInt(0) + if mode == "delete" { + deleteAfterPeriod = 0 + } + contact := false if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) { contact = true } // Use a map to speed up checking for deleted users later - userExists := map[string]bool{} + userExists := map[string]mediabrowser.User{} for _, user := range users { - userExists[user.ID] = true + userExists[user.ID] = user } for _, expiry := range app.storage.GetUserExpiries() { id := expiry.JellyfinID - if _, ok := userExists[id]; !ok { + user, ok := userExists[id] + if !ok { app.info.Printf(lm.DeleteExpiryForOldUser, id) app.storage.DeleteUserExpiryKey(expiry.JellyfinID) - } else if time.Now().After(expiry.Expiry) { - found := false - var user mediabrowser.User - for _, u := range users { - if u.ID == id { - found = true - user = u - break - } + continue + } + if !time.Now().After(expiry.Expiry) { + continue + } + deleteUserLater := deleteAfterPeriod != 0 && expiry.DeleteAfterPeriod + + // Record activity + activity := Activity{ + UserID: id, + SourceType: ActivityDaemon, + Time: time.Now(), + } + + deleteUserNow := deleteUserLater && time.Now().After(expiry.Expiry.AddDate(0, 0, deleteAfterPeriod)) + + if mode == "delete" || deleteUserNow { + app.info.Printf(lm.DeleteExpiredUser, user.Name) + deleted := false + err, deleted = app.DeleteUser(user) + // Silence unimportant errors + if deleted { + err = nil } - if !found { - app.storage.DeleteUserExpiryKey(expiry.JellyfinID) + activity.Type = ActivityDeletion + // Store the user name, since there's no longer a user ID to reference back to + activity.Value = user.Name + } else if mode == "disable" && !deleteUserLater { + app.info.Printf(lm.DisableExpiredUser, user.Name) + // Admins can't be disabled + // so they're not an admin anymore, sorry + user.Policy.IsAdministrator = false + err, _, _ = app.SetUserDisabled(user, true) + activity.Type = ActivityDisabled + } + if !(status == 200 || status == 204) || err != nil { + app.err.Printf(lm.FailedDeleteOrDisableExpiredUser, user.ID, err) + continue + } + + app.storage.SetActivityKey(shortuuid.New(), activity, nil, false) + + // If the we're not gonna be deleting the user later, we don't need this. + // also, if the admin re-enabled the user, we should get rid of the countdown. + if deleteAfterPeriod <= 0 || mode == "delete" || deleteUserNow || (deleteUserLater && !user.Policy.IsDisabled) { + app.storage.DeleteUserExpiryKey(user.ID) + } else if deleteAfterPeriod > 0 && !deleteUserLater { + expiry.DeleteAfterPeriod = true + app.storage.SetUserExpiryKey(user.ID, expiry) + } + app.jf.CacheExpiry = time.Now() + if contact { + if !ok { continue } - app.info.Printf(phrase, user.Name) - - // Record activity - activity := Activity{ - UserID: id, - SourceType: ActivityDaemon, - Time: time.Now(), - } - - if mode == "delete" { - deleted := false - err, deleted = app.DeleteUser(user) - // Silence unimportant errors - if deleted { - err = nil - } - activity.Type = ActivityDeletion - // Store the user name, since there's no longer a user ID to reference back to - activity.Value = user.Name - } else if mode == "disable" { - // Admins can't be disabled - // so they're not an admin anymore, sorry - user.Policy.IsAdministrator = false - err, _, _ = app.SetUserDisabled(user, true) - activity.Type = ActivityDisabled - } - if !(status == 200 || status == 204) || err != nil { - app.err.Printf(lm.FailedDeleteOrDisableExpiredUser, user.ID, err) - continue - } - - app.storage.SetActivityKey(shortuuid.New(), activity, nil, false) - - app.storage.DeleteUserExpiryKey(expiry.JellyfinID) - app.jf.CacheExpiry = time.Now() - if contact { - if !ok { - continue - } - name := app.getAddressOrName(user.ID) - msg, err := app.email.constructUserExpired(app, false) - if err != nil { - app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err) - } else if err := app.sendByID(msg, user.ID); err != nil { - app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, name, err) - } else { - app.err.Printf(lm.SentExpiryMessage, user.ID, name) - } + name := app.getAddressOrName(user.ID) + msg, err := app.email.constructUserExpired(app, false) + if err != nil { + app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err) + } else if err := app.sendByID(msg, user.ID); err != nil { + app.err.Printf(lm.FailedSendExpiryMessage, user.ID, name, err) + } else { + app.err.Printf(lm.SentExpiryMessage, user.ID, name) } } }