From 815721adb2883ea4f1b6d7316c390651c4c0fb3e Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 28 Nov 2025 12:06:17 +0000 Subject: [PATCH] profiles: add routes for viewing/modifying most of Profile struct is now in ProfileDTO (which is embedded in the former), admin can pull it from /profiles/raw/{name} GET, and replace it with PUT. Will make a ui for this. --- Makefile | 2 +- api-profiles.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++- router.go | 2 ++ storage.go | 34 ++++++++++++++++----------- 4 files changed, 84 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 60bb6a7..6018341 100644 --- a/Makefile +++ b/Makefile @@ -148,7 +148,7 @@ SWAGGER_SRC = $(wildcard api*.go) $(wildcard *auth.go) views.go SWAGGER_TARGET = docs/docs.go $(SWAGGER_TARGET): $(SWAGGER_SRC) $(SWAGINSTALL) - swag init -g main.go + swag init --parseDependency --parseInternal -g main.go VARIANTS_SRC = $(wildcard html/*.html) VARIANTS_TARGET = $(DATA)/html/admin.html diff --git a/api-profiles.go b/api-profiles.go index 8c27e3b..a8e3f50 100644 --- a/api-profiles.go +++ b/api-profiles.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "net/url" "time" "github.com/gin-gonic/gin" @@ -69,6 +70,65 @@ func (app *appContext) GetProfiles(gc *gin.Context) { gc.JSON(200, out) } +// @Summary Get the raw values stored in a profile (Configuration, Policy, Jellyseerr/Ombi if applicable, etc.). +// @Produce json +// @Success 200 {object} ProfileDTO +// @Failure 400 {object} boolResponse +// @Param name path string true "name of profile (url encoded if necessary)" +// @Router /profiles/raw/{name} [get] +// @Security Bearer +// @tags Users +func (app *appContext) GetRawProfile(gc *gin.Context) { + escapedName := gc.Param("name") + name, err := url.QueryUnescape(escapedName) + if err != nil { + respondBool(400, false, gc) + return + } + if profile, ok := app.storage.GetProfileKey(name); ok { + gc.JSON(200, profile.ProfileDTO) + return + } + respondBool(400, false, gc) +} + +// @Summary Update the raw data of a profile (Configuration, Policy, Jellyseerr/Ombi if applicable, etc.). +// @Produce json +// @Param ProfileDTO body ProfileDTO true "Raw profile data (all of it, do not omit anything)" +// @Success 200 {object} boolResponse +// @Failure 400 {object} boolResponse +// @Router /profiles/raw/{name} [put] +// @Security Bearer +// @tags Profiles & Settings +func (app *appContext) ReplaceRawProfile(gc *gin.Context) { + escapedName := gc.Param("name") + name, err := url.QueryUnescape(escapedName) + if err != nil { + respondBool(400, false, gc) + return + } + existingProfile, ok := app.storage.GetProfileKey(name) + if !ok { + respondBool(400, false, gc) + return + } + var req ProfileDTO + gc.BindJSON(&req) + existingProfile.ProfileDTO = req + if req.Name == "" { + req.Name = name + } + app.storage.SetProfileKey(req.Name, existingProfile) + if req.Name != name { + // Name change + app.storage.DeleteProfileKey(name) + if discordEnabled { + app.discord.UpdateCommands() + } + } + respondBool(200, true, gc) +} + // @Summary Set the default profile to use. // @Produce json // @Param profileChangeDTO body profileChangeDTO true "Default profile object" @@ -119,7 +179,7 @@ func (app *appContext) CreateProfile(gc *gin.Context) { } profile := Profile{ FromUser: user.Name, - Policy: user.Policy, + ProfileDTO: ProfileDTO{Policy: user.Policy}, Homescreen: req.Homescreen, } app.debug.Printf(lm.CreateProfileFromUser, user.Name) diff --git a/router.go b/router.go index 6f113bb..077dee3 100644 --- a/router.go +++ b/router.go @@ -214,6 +214,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) { api.POST(p+"/invites/profile", app.SetProfile) api.GET(p+"/profiles", app.GetProfiles) api.GET(p+"/profiles/names", app.GetProfileNames) + api.GET(p+"/profiles/raw/:name", app.GetRawProfile) + api.PUT(p+"/profiles/raw/:name", app.ReplaceRawProfile) api.POST(p+"/profiles/default", app.SetDefaultProfile) api.POST(p+"/profiles", app.CreateProfile) api.DELETE(p+"/profiles", app.DeleteProfile) diff --git a/storage.go b/storage.go index 58932da..232e53a 100644 --- a/storage.go +++ b/storage.go @@ -748,20 +748,24 @@ type userPageContent struct { // timePattern: %Y-%m-%dT%H:%M:%S.%f type Profile struct { - Name string `badgerhold:"key"` - Admin bool `json:"admin,omitempty" badgerhold:"index"` - LibraryAccess string `json:"libraries,omitempty"` - FromUser string `json:"fromUser,omitempty"` - Homescreen bool `json:"homescreen"` - Policy mediabrowser.Policy `json:"policy,omitempty"` - Configuration mediabrowser.Configuration `json:"configuration,omitempty"` - Displayprefs map[string]interface{} `json:"displayprefs,omitempty"` - Default bool `json:"default,omitempty"` - Ombi map[string]interface{} `json:"ombi,omitempty"` - Jellyseerr JellyseerrTemplate `json:"jellyseerr,omitempty"` + ProfileDTO + Admin bool `json:"admin,omitempty" badgerhold:"index"` + LibraryAccess string `json:"libraries,omitempty"` + FromUser string `json:"fromUser,omitempty"` + Homescreen bool `json:"homescreen"` + Default bool `json:"default,omitempty"` ReferralTemplateKey string } +type ProfileDTO struct { + Name string `badgerhold:"key" json:"name"` + Policy mediabrowser.Policy `json:"policy,omitempty"` + Configuration mediabrowser.Configuration `json:"configuration,omitempty"` + Displayprefs map[string]any `json:"displayprefs,omitempty"` + Ombi map[string]any `json:"ombi,omitempty"` + Jellyseerr JellyseerrTemplate `json:"jellyseerr,omitempty"` +} + type JellyseerrTemplate struct { Enabled bool `json:"enabled,omitempty"` User jellyseerr.UserTemplate `json:"user,omitempty"` @@ -1662,9 +1666,11 @@ func (st *Storage) migrateToProfile() error { st.loadDisplayprefs() st.loadProfiles() st.deprecatedProfiles["Default"] = Profile{ - Policy: st.deprecatedPolicy, - Configuration: st.deprecatedConfiguration, - Displayprefs: st.deprecatedDisplayprefs, + ProfileDTO: ProfileDTO{ + Policy: st.deprecatedPolicy, + Configuration: st.deprecatedConfiguration, + Displayprefs: st.deprecatedDisplayprefs, + }, } return st.storeProfiles() }