diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 6167393..6d75f97 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -35,6 +35,7 @@ type api struct { calls *callsAPI users *usersAPI incidents *incidentsAPI + prefs *prefsAPI } func (a *api) ShareRouter() http.Handler { @@ -48,6 +49,7 @@ func New(baseURL url.URL) *api { calls: new(callsAPI), incidents: newIncidentsAPI(&baseURL), users: new(usersAPI), + prefs: new(prefsAPI), } s.shares = newShareAPI(&baseURL, s.shareHandlers()) return s @@ -61,6 +63,7 @@ func (a *api) Subrouter() http.Handler { r.Mount("/call", a.calls.Subrouter()) r.Mount("/incident", a.incidents.Subrouter()) r.Mount("/share", a.shares.Subrouter()) + r.Mount("/prefs", a.prefs.Subrouter()) return r } diff --git a/pkg/rest/prefs.go b/pkg/rest/prefs.go new file mode 100644 index 0000000..95a8853 --- /dev/null +++ b/pkg/rest/prefs.go @@ -0,0 +1,128 @@ +package rest + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "strings" + + "dynatron.me/x/stillbox/pkg/auth" + "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/settings" + "dynatron.me/x/stillbox/pkg/users" + + "github.com/go-chi/chi/v5" +) + +var ( + ErrBadAppName = errors.New("bad app name") +) + +type prefsAPI struct { +} + +func (pa *prefsAPI) Subrouter() http.Handler { + r := chi.NewMux() + + r.Get(`/{appName}`, pa.getPrefs) + r.Put(`/{appName}`, pa.putPrefs) + + return r +} + +func (pa *prefsAPI) getPrefs(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + username := auth.UsernameFrom(ctx) + + if username == nil { + wErr(w, r, autoError(rbac.ErrBadSubject)) + return + } + + p := struct { + AppName *string `param:"appName"` + }{} + + err := decodeParams(&p, r) + if err != nil { + wErr(w, r, badRequest(err)) + return + } + + if p.AppName == nil { + wErr(w, r, autoError(ErrBadAppName)) + return + } + + us := users.FromCtx(ctx) + prefs, err := us.UserPrefs(ctx, *username, *p.AppName) + if err != nil { + wErr(w, r, autoError(err)) + return + } + + sysPrefs, err := settings.FromCtx(ctx).GetPrefs(ctx, *p.AppName) + if err != nil { + wErr(w, r, autoError(err)) + return + } + + po := struct { + User json.RawMessage `json:"userPrefs"` + System json.RawMessage `json:"sysPrefs"` + }{ + User: prefs, + System: sysPrefs, + } + + respond(w, r, po) +} + +func (pa *prefsAPI) putPrefs(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + username := auth.UsernameFrom(ctx) + + if username == nil { + wErr(w, r, autoError(rbac.ErrBadSubject)) + return + } + + contentType := strings.Split(r.Header.Get("Content-Type"), ";")[0] + if contentType != "application/json" { + wErr(w, r, badRequest(errors.New("only json accepted"))) + return + } + + p := struct { + AppName *string `param:"appName"` + }{} + + err := decodeParams(&p, r) + if err != nil { + wErr(w, r, badRequest(err)) + return + } + + if p.AppName == nil { + wErr(w, r, autoError(ErrBadAppName)) + return + } + + prefs, err := io.ReadAll(r.Body) + if err != nil { + wErr(w, r, autoError(err)) + return + } + + us := users.FromCtx(ctx) + err = us.SetUserPrefs(ctx, *username, *p.AppName, prefs) + if err != nil { + wErr(w, r, autoError(err)) + return + } + + _, _ = w.Write(prefs) +} diff --git a/pkg/rest/users.go b/pkg/rest/users.go index 1fa6703..0fb816d 100644 --- a/pkg/rest/users.go +++ b/pkg/rest/users.go @@ -1,112 +1,21 @@ package rest import ( - "errors" - "io" "net/http" - "strings" - - "dynatron.me/x/stillbox/pkg/auth" - "dynatron.me/x/stillbox/pkg/rbac" - "dynatron.me/x/stillbox/pkg/users" "github.com/go-chi/chi/v5" ) -var ( - ErrBadAppName = errors.New("bad app name") -) - type usersAPI struct { } func (ua *usersAPI) Subrouter() http.Handler { r := chi.NewMux() - r.Get(`/prefs/{appName}`, ua.getPrefs) - r.Put(`/prefs/{appName}`, ua.putPrefs) + r.Get("/{user}", ua.getUser) return r } -func (ua *usersAPI) getPrefs(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - username := auth.UsernameFrom(ctx) - - if username == nil { - wErr(w, r, autoError(rbac.ErrBadSubject)) - return - } - - p := struct { - AppName *string `param:"appName"` - }{} - - err := decodeParams(&p, r) - if err != nil { - wErr(w, r, badRequest(err)) - return - } - - if p.AppName == nil { - wErr(w, r, autoError(ErrBadAppName)) - return - } - - us := users.FromCtx(ctx) - prefs, err := us.UserPrefs(ctx, *username, *p.AppName) - if err != nil { - wErr(w, r, autoError(err)) - return - } - - _, _ = w.Write(prefs) -} - -func (ua *usersAPI) putPrefs(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - username := auth.UsernameFrom(ctx) - - if username == nil { - wErr(w, r, autoError(rbac.ErrBadSubject)) - return - } - - contentType := strings.Split(r.Header.Get("Content-Type"), ";")[0] - if contentType != "application/json" { - wErr(w, r, badRequest(errors.New("only json accepted"))) - return - } - - p := struct { - AppName *string `param:"appName"` - }{} - - err := decodeParams(&p, r) - if err != nil { - wErr(w, r, badRequest(err)) - return - } - - if p.AppName == nil { - wErr(w, r, autoError(ErrBadAppName)) - return - } - - prefs, err := io.ReadAll(r.Body) - if err != nil { - wErr(w, r, autoError(err)) - return - } - - us := users.FromCtx(ctx) - err = us.SetUserPrefs(ctx, *username, *p.AppName, prefs) - if err != nil { - wErr(w, r, autoError(err)) - return - } - - _, _ = w.Write(prefs) +func (ua *usersAPI) getUser(w http.ResponseWriter, r *http.Request) { } diff --git a/pkg/server/server.go b/pkg/server/server.go index 0555819..3fcb0dc 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -179,6 +179,7 @@ func (s *Server) fillCtx(ctx context.Context) context.Context { ctx = shares.CtxWithStore(ctx, s.share) ctx = rbac.CtxWithRBAC(ctx, s.rbac) ctx = stats.CtxWithStats(ctx, s.stats) + ctx = settings.CtxWithStore(ctx, s.settings) return ctx } diff --git a/pkg/settings/defaults.go b/pkg/settings/defaults.go index b23256c..654247a 100644 --- a/pkg/settings/defaults.go +++ b/pkg/settings/defaults.go @@ -1,7 +1,20 @@ package settings -type Defaults map[string]Setting +import "encoding/json" + +type Defaults map[Setting]Setting + +func MustMarshal(s Setting) json.RawMessage { + b, err := json.Marshal(s) + if err != nil { + panic(err) + } + + return b +} var ConfigDefaults = Defaults{ - "calls.view.showSourceAlias": false, + prefsName("stillbox"): MustMarshal(Defaults{ + "calls.view.showSourceAlias": false, + }), } diff --git a/pkg/settings/settings.go b/pkg/settings/settings.go index a5330d9..5ba376a 100644 --- a/pkg/settings/settings.go +++ b/pkg/settings/settings.go @@ -21,6 +21,12 @@ type Store interface { // Get gets a setting and unmarshals it into dst. Get(ctx context.Context, name string) (Setting, error) + // GetPrefs gets system prefs for an app name. + GetPrefs(ctx context.Context, appName string) (json.RawMessage, error) + + // SetPrefs gets system prefs for an app name. + SetPrefs(ctx context.Context, appName string, val Setting) error + // Set sets a setting. Set(ctx context.Context, name string, val Setting) error @@ -62,6 +68,30 @@ func New(defaults Defaults) *postgresStore { return s } +func (s *postgresStore) defaultPrefs(appName string) json.RawMessage { + d, has := s.defaults[prefsName(appName)].(json.RawMessage) + if has { + return d + } + + return nil +} + +func (s *postgresStore) getJSONB(ctx context.Context, name string) (json.RawMessage, error) { + db := database.FromCtx(ctx) + + sRes, err := db.GetSetting(ctx, name) + if err != nil && database.IsNoRows(err) { + return nil, ErrNoSetting + } + + return sRes, err +} + +func prefsName(appName string) string { + return "prefs." + appName +} + func (s *postgresStore) Get(ctx context.Context, name string) (Setting, error) { _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionRead)) if err != nil { @@ -72,23 +102,17 @@ func (s *postgresStore) Get(ctx context.Context, name string) (Setting, error) { if has { return ci, nil } - db := database.FromCtx(ctx) - - cBytes, err := db.GetSetting(ctx, name) - if err != nil { - if database.IsNoRows(err) { - def, hasDefault := s.defaults[name] - if hasDefault { - return def, nil - } - - return nil, ErrNoSetting + sRes, err := s.getJSONB(ctx, name) + if err != nil && errors.Is(err, ErrNoSetting) { + def, hasDefault := s.defaults[name] + if hasDefault { + return def, nil } return nil, err } - err = json.Unmarshal(cBytes, ci) + err = json.Unmarshal(sRes, &ci) if err != nil { return nil, err } @@ -98,6 +122,40 @@ func (s *postgresStore) Get(ctx context.Context, name string) (Setting, error) { return ci, nil } +func (s *postgresStore) GetPrefs(ctx context.Context, appName string) (json.RawMessage, error) { + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionRead)) + if err != nil { + return nil, err + } + + prName := prefsName(appName) + ci, has := s.c.Get(prName) + if has { + rm, isRM := ci.(json.RawMessage) + if isRM { + return rm, nil + } + } + + p, err := s.getJSONB(ctx, prName) + if errors.Is(err, ErrNoSetting) { + def, hasDefault := s.defaults[prName].(json.RawMessage) + if hasDefault { + return def, nil + } + + return nil, err + } + + s.c.Set(prName, p) + + return p, nil +} + +func (s *postgresStore) SetPrefs(ctx context.Context, appName string, val Setting) error { + return s.Set(ctx, prefsName(appName), val) +} + func (s *postgresStore) Set(ctx context.Context, name string, val Setting) error { subj, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionCreate, entities.ActionUpdate)) if err != nil {