From b620c0d9ae2c63025c705f58b5902388f2a9b501 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 19 Oct 2023 18:56:35 +0100 Subject: [PATCH] activity: implement most initial logging resetPassword, changePassword, delete/createInvite, enable/disable, creation/deletion of invites & users are all done, only remaining one is account linking. --- api-invites.go | 39 +++++++++++++++++++++++++++++++++++++++ api-userpage.go | 10 ++++++++++ api-users.go | 24 ++++++++++++++++++++++++ api.go | 11 +++++++++++ storage.go | 9 +++++---- userdaemon.go | 14 ++++++++++++++ views.go | 17 +++++++++++++++++ 7 files changed, 120 insertions(+), 4 deletions(-) diff --git a/api-invites.go b/api-invites.go index 14c5f45..e6d92f4 100644 --- a/api-invites.go +++ b/api-invites.go @@ -85,6 +85,13 @@ func (app *appContext) checkInvites() { wait.Wait() } app.storage.DeleteInvitesKey(data.Code) + + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityDeleteInvite, + SourceType: ActivityDaemon, + InviteCode: data.Code, + Time: time.Now(), + }) } } @@ -130,12 +137,24 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool } match = false app.storage.DeleteInvitesKey(code) + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityDeleteInvite, + SourceType: ActivityDaemon, + InviteCode: code, + Time: time.Now(), + }) } else if used { del := false newInv := inv if newInv.RemainingUses == 1 { del = true app.storage.DeleteInvitesKey(code) + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityDeleteInvite, + SourceType: ActivityDaemon, + InviteCode: code, + Time: time.Now(), + }) } else if newInv.RemainingUses != 0 { // 0 means infinite i guess? newInv.RemainingUses-- @@ -236,6 +255,17 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { } } app.storage.SetInvitesKey(invite.Code, invite) + + // Record activity + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityCreateInvite, + UserID: "", + SourceType: ActivityAdmin, + Source: gc.GetString("jfId"), + InviteCode: invite.Code, + Time: time.Now(), + }) + respondBool(200, true, gc) } @@ -433,6 +463,15 @@ func (app *appContext) DeleteInvite(gc *gin.Context) { _, ok = app.storage.GetInvitesKey(req.Code) if ok { app.storage.DeleteInvitesKey(req.Code) + + // Record activity + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityDeleteInvite, + SourceType: ActivityAdmin, + Source: gc.GetString("jfId"), + Time: time.Now(), + }) + app.info.Printf("%s: Invite deleted", req.Code) respondBool(200, true, gc) return diff --git a/api-userpage.go b/api-userpage.go index 0df5653..5ccb46d 100644 --- a/api-userpage.go +++ b/api-userpage.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" + "github.com/lithammer/shortuuid/v3" "github.com/timshannon/badgerhold/v4" ) @@ -620,6 +621,15 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) { respondBool(500, false, gc) return } + + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityChangePassword, + UserID: user.ID, + SourceType: ActivityUser, + Source: user.ID, + Time: time.Now(), + }) + if app.config.Section("ombi").Key("enabled").MustBool(false) { func() { ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId")) diff --git a/api-users.go b/api-users.go index 11702f2..0d2eb06 100644 --- a/api-users.go +++ b/api-users.go @@ -567,6 +567,10 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) { sendMail = false } } + activityType := ActivityDisabled + if req.Enabled { + activityType = ActivityEnabled + } for _, userID := range req.Users { user, status, err := app.jf.UserByID(userID, false) if status != 200 || err != nil { @@ -581,6 +585,16 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) { app.err.Printf("Failed to set policy for user \"%s\" (%d): %v", userID, status, err) continue } + + // Record activity + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: activityType, + UserID: userID, + SourceType: ActivityAdmin, + Source: gc.GetString("jfId"), + Time: time.Now(), + }) + if sendMail && req.Notify { if err := app.sendByID(msg, userID); err != nil { app.err.Printf("Failed to send account enabled/disabled email: %v", err) @@ -642,6 +656,16 @@ func (app *appContext) DeleteUsers(gc *gin.Context) { errors[userID] += msg } } + + // Record activity + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityDeletion, + UserID: userID, + SourceType: ActivityAdmin, + Source: gc.GetString("jfId"), + Time: time.Now(), + }) + if sendMail && req.Notify { if err := app.sendByID(msg, userID); err != nil { app.err.Printf("Failed to send account deletion email: %v", err) diff --git a/api.go b/api.go index d9d44e4..1b926e0 100644 --- a/api.go +++ b/api.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/hrfee/mediabrowser" "github.com/itchyny/timefmt-go" + "github.com/lithammer/shortuuid/v3" "gopkg.in/ini.v1" ) @@ -157,6 +158,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { } username = resp.UsersReset[0] } + var user mediabrowser.User var status int var err error @@ -170,6 +172,15 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) { respondBool(500, false, gc) return } + + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityResetPassword, + UserID: user.ID, + SourceType: ActivityUser, + Source: user.ID, + Time: time.Now(), + }) + prevPassword := req.PIN if isInternal { prevPassword = "" diff --git a/storage.go b/storage.go index 9534d13..0f00db0 100644 --- a/storage.go +++ b/storage.go @@ -38,14 +38,15 @@ const ( type ActivitySource int const ( - ActivityUser ActivitySource = iota // Source = UserID. For ActivityCreation, this would mean the referrer. - ActivityAdmin // Source = Admin's UserID, or simply just "admin" - ActivityAnon // Source = Blank, or potentially browser info. For ActivityCreation, this would be via an invite + ActivityUser ActivitySource = iota // Source = UserID. For ActivityCreation, this would mean the referrer. + ActivityAdmin // Source = Admin's UserID, or blank if jellyfin login isn't on. + ActivityAnon // Source = Blank, or potentially browser info. For ActivityCreation, this would be via an invite + ActivityDaemon // Source = Blank, was deleted/disabled due to expiry by daemon ) type Activity struct { Type ActivityType `badgerhold:"index"` - UserID string + UserID string // ID of target user. For account creation, this will be the newly created account SourceType ActivitySource Source string InviteCode string // Only set for ActivityCreation diff --git a/userdaemon.go b/userdaemon.go index de0e0ce..5ac7dcd 100644 --- a/userdaemon.go +++ b/userdaemon.go @@ -4,6 +4,7 @@ import ( "time" "github.com/hrfee/mediabrowser" + "github.com/lithammer/shortuuid/v3" ) type userDaemon struct { @@ -95,18 +96,31 @@ func (app *appContext) checkUsers() { continue } app.info.Printf("%s expired user \"%s\"", termPlural, user.Name) + + // Record activity + activity := Activity{ + UserID: id, + SourceType: ActivityDaemon, + Time: time.Now(), + } + if mode == "delete" { status, err = app.jf.DeleteUser(id) + activity.Type = ActivityDeletion } else if mode == "disable" { user.Policy.IsDisabled = true // Admins can't be disabled user.Policy.IsAdministrator = false status, err = app.jf.SetPolicy(id, user.Policy) + activity.Type = ActivityDisabled } if !(status == 200 || status == 204) || err != nil { app.err.Printf("Failed to %s \"%s\" (%d): %s", mode, user.Name, status, err) continue } + + app.storage.SetActivityKey(shortuuid.New(), activity) + app.storage.DeleteUserExpiryKey(expiry.JellyfinID) app.jf.CacheExpiry = time.Now() if contact { diff --git a/views.go b/views.go index 7e56900..2d91e21 100644 --- a/views.go +++ b/views.go @@ -17,6 +17,7 @@ import ( "github.com/golang-jwt/jwt" "github.com/gomarkdown/markdown" "github.com/hrfee/mediabrowser" + "github.com/lithammer/shortuuid/v3" "github.com/steambap/captcha" ) @@ -329,6 +330,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) { } username = pwr.Username } + if (status == 200 || status == 204) && err == nil && (isInternal || resp.Success) { data["success"] = true data["pin"] = pin @@ -338,6 +340,21 @@ func (app *appContext) ResetPassword(gc *gin.Context) { } else { app.err.Printf("Password Reset failed (%d): %v", status, err) } + + // Only log PWRs we know the user for. + if username != "" { + jfUser, status, err := app.jf.UserByName(username, false) + if err == nil && status == 200 { + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityResetPassword, + UserID: jfUser.ID, + SourceType: ActivityUser, + Source: jfUser.ID, + Time: time.Now(), + }) + } + } + if app.config.Section("ombi").Key("enabled").MustBool(false) { jfUser, status, err := app.jf.UserByName(username, false) if status != 200 || err != nil {