From a735e4ff291af6caacaf74452e1a6c7baed60e6f Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sat, 24 Jun 2023 21:29:16 +0100 Subject: [PATCH] db: migrate user profiles --- api-invites.go | 26 ++++++++++--------- api-ombi.go | 18 +++---------- api-profiles.go | 37 ++++++++++---------------- api-users.go | 18 ++++++------- lang/admin/en-us.json | 2 +- migrations.go | 5 ++++ storage.go | 60 ++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 105 insertions(+), 61 deletions(-) diff --git a/api-invites.go b/api-invites.go index 61e6772..e3d6cd0 100644 --- a/api-invites.go +++ b/api-invites.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/itchyny/timefmt-go" "github.com/lithammer/shortuuid/v3" + "github.com/timshannon/badgerhold/v4" ) func (app *appContext) checkInvites() { @@ -202,7 +203,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { } } if req.Profile != "" { - if _, ok := app.storage.profiles[req.Profile]; ok { + if _, ok := app.storage.GetProfileKey(req.Profile); ok { invite.Profile = req.Profile } else { invite.Profile = "Default" @@ -283,17 +284,18 @@ func (app *appContext) GetInvites(gc *gin.Context) { } invites = append(invites, invite) } - profiles := make([]string, len(app.storage.profiles)) - if len(app.storage.profiles) != 0 { - profiles[0] = app.storage.defaultProfile + fullProfileList := app.storage.GetProfiles() + profiles := make([]string, len(fullProfileList)) + if len(profiles) != 0 { + defaultProfile := app.storage.GetDefaultProfile() + profiles[0] = defaultProfile.Name i := 1 - if len(app.storage.profiles) > 1 { - for p := range app.storage.profiles { - if p != app.storage.defaultProfile { - profiles[i] = p - i++ - } - } + if len(fullProfileList) > 1 { + app.storage.db.ForEach(badgerhold.Where("Name").Ne(profiles[0]), func(p *Profile) error { + profiles[i] = p.Name + i++ + return nil + }) } } resp := getInvitesDTO{ @@ -316,7 +318,7 @@ func (app *appContext) SetProfile(gc *gin.Context) { gc.BindJSON(&req) app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile) // "" means "Don't apply profile" - if _, ok := app.storage.profiles[req.Profile]; !ok && req.Profile != "" { + if _, ok := app.storage.GetProfileKey(req.Profile); !ok && req.Profile != "" { app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile) respond(500, "Profile not found", gc) return diff --git a/api-ombi.go b/api-ombi.go index 40a8f72..0113885 100644 --- a/api-ombi.go +++ b/api-ombi.go @@ -71,7 +71,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) { var req ombiUser gc.BindJSON(&req) profileName := gc.Param("profile") - profile, ok := app.storage.profiles[profileName] + profile, ok := app.storage.GetProfileKey(profileName) if !ok { respondBool(400, false, gc) return @@ -83,12 +83,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) { return } profile.Ombi = template - app.storage.profiles[profileName] = profile - if err := app.storage.storeProfiles(); err != nil { - respond(500, "Failed to store profile", gc) - app.err.Printf("Failed to store profiles: %v", err) - return - } + app.storage.SetProfileKey(profileName, profile) respondBool(204, true, gc) } @@ -103,17 +98,12 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) { // @tags Ombi func (app *appContext) DeleteOmbiProfile(gc *gin.Context) { profileName := gc.Param("profile") - profile, ok := app.storage.profiles[profileName] + profile, ok := app.storage.GetProfileKey(profileName) if !ok { respondBool(400, false, gc) return } profile.Ombi = nil - app.storage.profiles[profileName] = profile - if err := app.storage.storeProfiles(); err != nil { - respond(500, "Failed to store profile", gc) - app.err.Printf("Failed to store profiles: %v", err) - return - } + app.storage.SetProfileKey(profileName, profile) respondBool(204, true, gc) } diff --git a/api-profiles.go b/api-profiles.go index 475857d..579cdff 100644 --- a/api-profiles.go +++ b/api-profiles.go @@ -4,6 +4,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/timshannon/badgerhold/v4" ) // @Summary Get a list of profiles @@ -13,14 +14,13 @@ import ( // @Security Bearer // @tags Profiles & Settings func (app *appContext) GetProfiles(gc *gin.Context) { - app.storage.loadProfiles() app.debug.Println("Profiles requested") out := getProfilesDTO{ - DefaultProfile: app.storage.defaultProfile, + DefaultProfile: app.storage.GetDefaultProfile().Name, Profiles: map[string]profileDTO{}, } - for name, p := range app.storage.profiles { - out.Profiles[name] = profileDTO{ + for _, p := range app.storage.GetProfiles() { + out.Profiles[p.Name] = profileDTO{ Admin: p.Admin, LibraryAccess: p.LibraryAccess, FromUser: p.FromUser, @@ -42,20 +42,20 @@ func (app *appContext) SetDefaultProfile(gc *gin.Context) { req := profileChangeDTO{} gc.BindJSON(&req) app.info.Printf("Setting default profile to \"%s\"", req.Name) - if _, ok := app.storage.profiles[req.Name]; !ok { + if _, ok := app.storage.GetProfileKey(req.Name); !ok { app.err.Printf("Profile not found: \"%s\"", req.Name) respond(500, "Profile not found", gc) return } - for name, profile := range app.storage.profiles { - if name == req.Name { - profile.Admin = true - app.storage.profiles[name] = profile + app.storage.db.ForEach(&badgerhold.Query{}, func(profile *Profile) error { + if profile.Name == req.Name { + profile.Default = true } else { - profile.Admin = false + profile.Default = false } - } - app.storage.defaultProfile = req.Name + app.storage.SetProfileKey(profile.Name, *profile) + return nil + }) respondBool(200, true, gc) } @@ -92,10 +92,7 @@ func (app *appContext) CreateProfile(gc *gin.Context) { return } } - app.storage.loadProfiles() - app.storage.profiles[req.Name] = profile - app.storage.storeProfiles() - app.storage.loadProfiles() + app.storage.SetProfileKey(req.Name, profile) respondBool(200, true, gc) } @@ -110,12 +107,6 @@ func (app *appContext) DeleteProfile(gc *gin.Context) { req := profileChangeDTO{} gc.BindJSON(&req) name := req.Name - if _, ok := app.storage.profiles[name]; ok { - if app.storage.defaultProfile == name { - app.storage.defaultProfile = "" - } - delete(app.storage.profiles, name) - } - app.storage.storeProfiles() + app.storage.DeleteProfileKey(name) respondBool(200, true, gc) } diff --git a/api-users.go b/api-users.go index 71d7b30..0117457 100644 --- a/api-users.go +++ b/api-users.go @@ -269,7 +269,6 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc success = false return } - app.storage.loadProfiles() invite, _ := app.storage.GetInvitesKey(req.Code) app.checkInvite(req.Code, true, req.Username) if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) { @@ -301,9 +300,9 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc if invite.Profile != "" { app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile) var ok bool - profile, ok = app.storage.profiles[invite.Profile] + profile, ok = app.storage.GetProfileKey(invite.Profile) if !ok { - profile = app.storage.profiles["Default"] + profile = app.storage.GetDefaultProfile() } if profile.Policy.BlockedTags != nil { app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile) @@ -1008,25 +1007,24 @@ func (app *appContext) ApplySettings(gc *gin.Context) { var displayprefs map[string]interface{} var ombi map[string]interface{} if req.From == "profile" { - app.storage.loadProfiles() // Check profile exists & isn't empty - if _, ok := app.storage.profiles[req.Profile]; !ok || app.storage.profiles[req.Profile].Policy.BlockedTags == nil { + profile, ok := app.storage.GetProfileKey(req.Profile) + if !ok || profile.Policy.BlockedTags == nil { app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile) respond(500, "Couldn't find profile", gc) return } if req.Homescreen { - if app.storage.profiles[req.Profile].Configuration.GroupedFolders == nil || len(app.storage.profiles[req.Profile].Displayprefs) == 0 { + if profile.Configuration.GroupedFolders == nil || len(profile.Displayprefs) == 0 { app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile) respond(500, "No homescreen template available", gc) return } - configuration = app.storage.profiles[req.Profile].Configuration - displayprefs = app.storage.profiles[req.Profile].Displayprefs + configuration = profile.Configuration + displayprefs = profile.Displayprefs } - policy = app.storage.profiles[req.Profile].Policy + policy = profile.Policy if app.config.Section("ombi").Key("enabled").MustBool(false) { - profile := app.storage.profiles[req.Profile] if profile.Ombi != nil && len(profile.Ombi) != 0 { ombi = profile.Ombi } diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index 3ff043e..db9773f 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -79,7 +79,7 @@ "ombiProfile": "Ombi user profile", "ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go when this profile is selected.", "userProfiles": "User Profiles", - "userProfilesDescription": "Profiles are applied to users when they create an account. A profile include library access rights and homescreen layout.", + "userProfilesDescription": "Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.", "userProfilesIsDefault": "Default", "userProfilesLibraries": "Libraries", "addProfile": "Add Profile", diff --git a/migrations.go b/migrations.go index 2a476da..8d73e05 100644 --- a/migrations.go +++ b/migrations.go @@ -236,6 +236,11 @@ func migrateToBadger(app *appContext) { for k, v := range app.storage.deprecatedUserExpiries { app.storage.SetUserExpiryKey(k, UserExpiry{Expiry: v}) } + + app.storage.loadProfiles() + for k, v := range app.storage.profiles { + app.storage.SetProfileKey(k, v) + } } // Migrate between hyphenated & non-hyphenated user IDs. Doesn't seem to happen anymore, so disabled. diff --git a/storage.go b/storage.go index 503dafd..dbe11e4 100644 --- a/storage.go +++ b/storage.go @@ -312,6 +312,63 @@ func (st *Storage) DeleteUserExpiryKey(k string) { st.db.Delete(k, UserExpiry{}) } +// GetProfiles returns a copy of the store. +func (st *Storage) GetProfiles() []Profile { + result := []Profile{} + err := st.db.Find(&result, &badgerhold.Query{}) + if err != nil { + // fmt.Printf("Failed to find profiles: %v\n", err) + } + return result +} + +// GetProfileKey returns the value stored in the store's key. +func (st *Storage) GetProfileKey(k string) (Profile, bool) { + result := Profile{} + err := st.db.Get(k, &result) + ok := true + if err != nil { + // fmt.Printf("Failed to find profile: %v\n", err) + ok = false + } + return result, ok +} + +// SetProfileKey stores value v in key k. +func (st *Storage) SetProfileKey(k string, v Profile) { + v.Name = k + v.Admin = v.Policy.IsAdministrator + if v.Policy.EnabledFolders != nil { + if len(v.Policy.EnabledFolders) == 0 { + v.LibraryAccess = "All" + } else { + v.LibraryAccess = strconv.Itoa(len(v.Policy.EnabledFolders)) + } + } + if v.FromUser == "" { + v.FromUser = "Unknown" + } + err := st.db.Upsert(k, v) + if err != nil { + // fmt.Printf("Failed to set profile: %v\n", err) + } +} + +// DeleteProfileKey deletes value at key k. +func (st *Storage) DeleteProfileKey(k string) { + st.db.Delete(k, Profile{}) +} + +// GetDefaultProfile returns the first profile set as default, or anything available if there isn't one. +func (st *Storage) GetDefaultProfile() Profile { + defaultProfile := Profile{} + err := st.db.FindOne(&defaultProfile, badgerhold.Where("Default").Eq(true)) + if err != nil { + st.db.FindOne(&defaultProfile, &badgerhold.Query{}) + } + return defaultProfile +} + type TelegramUser struct { JellyfinID string `badgerhold:"key"` ChatID int64 `badgerhold:"index"` @@ -366,7 +423,8 @@ type userPageContent struct { // timePattern: %Y-%m-%dT%H:%M:%S.%f type Profile struct { - Admin bool `json:"admin,omitempty"` + Name string `badgerhold:"key"` + Admin bool `json:"admin,omitempty" badgerhold:"index"` LibraryAccess string `json:"libraries,omitempty"` FromUser string `json:"fromUser,omitempty"` Policy mediabrowser.Policy `json:"policy,omitempty"`