Initial settings

This commit is contained in:
Daniel Ponte 2025-02-25 21:42:07 -05:00
parent b275881697
commit c6ca856635
6 changed files with 219 additions and 107 deletions

View file

@ -35,6 +35,7 @@ type api struct {
calls *callsAPI calls *callsAPI
users *usersAPI users *usersAPI
incidents *incidentsAPI incidents *incidentsAPI
prefs *prefsAPI
} }
func (a *api) ShareRouter() http.Handler { func (a *api) ShareRouter() http.Handler {
@ -48,6 +49,7 @@ func New(baseURL url.URL) *api {
calls: new(callsAPI), calls: new(callsAPI),
incidents: newIncidentsAPI(&baseURL), incidents: newIncidentsAPI(&baseURL),
users: new(usersAPI), users: new(usersAPI),
prefs: new(prefsAPI),
} }
s.shares = newShareAPI(&baseURL, s.shareHandlers()) s.shares = newShareAPI(&baseURL, s.shareHandlers())
return s return s
@ -61,6 +63,7 @@ func (a *api) Subrouter() http.Handler {
r.Mount("/call", a.calls.Subrouter()) r.Mount("/call", a.calls.Subrouter())
r.Mount("/incident", a.incidents.Subrouter()) r.Mount("/incident", a.incidents.Subrouter())
r.Mount("/share", a.shares.Subrouter()) r.Mount("/share", a.shares.Subrouter())
r.Mount("/prefs", a.prefs.Subrouter())
return r return r
} }

128
pkg/rest/prefs.go Normal file
View file

@ -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)
}

View file

@ -1,112 +1,21 @@
package rest package rest
import ( import (
"errors"
"io"
"net/http" "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" "github.com/go-chi/chi/v5"
) )
var (
ErrBadAppName = errors.New("bad app name")
)
type usersAPI struct { type usersAPI struct {
} }
func (ua *usersAPI) Subrouter() http.Handler { func (ua *usersAPI) Subrouter() http.Handler {
r := chi.NewMux() r := chi.NewMux()
r.Get(`/prefs/{appName}`, ua.getPrefs) r.Get("/{user}", ua.getUser)
r.Put(`/prefs/{appName}`, ua.putPrefs)
return r return r
} }
func (ua *usersAPI) getPrefs(w http.ResponseWriter, r *http.Request) { func (ua *usersAPI) getUser(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)
} }

View file

@ -179,6 +179,7 @@ func (s *Server) fillCtx(ctx context.Context) context.Context {
ctx = shares.CtxWithStore(ctx, s.share) ctx = shares.CtxWithStore(ctx, s.share)
ctx = rbac.CtxWithRBAC(ctx, s.rbac) ctx = rbac.CtxWithRBAC(ctx, s.rbac)
ctx = stats.CtxWithStats(ctx, s.stats) ctx = stats.CtxWithStats(ctx, s.stats)
ctx = settings.CtxWithStore(ctx, s.settings)
return ctx return ctx
} }

View file

@ -1,7 +1,20 @@
package settings 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{ var ConfigDefaults = Defaults{
prefsName("stillbox"): MustMarshal(Defaults{
"calls.view.showSourceAlias": false, "calls.view.showSourceAlias": false,
}),
} }

View file

@ -21,6 +21,12 @@ type Store interface {
// Get gets a setting and unmarshals it into dst. // Get gets a setting and unmarshals it into dst.
Get(ctx context.Context, name string) (Setting, error) 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 sets a setting.
Set(ctx context.Context, name string, val Setting) error Set(ctx context.Context, name string, val Setting) error
@ -62,6 +68,30 @@ func New(defaults Defaults) *postgresStore {
return s 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) { func (s *postgresStore) Get(ctx context.Context, name string) (Setting, error) {
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionRead)) _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionRead))
if err != nil { if err != nil {
@ -72,23 +102,17 @@ func (s *postgresStore) Get(ctx context.Context, name string) (Setting, error) {
if has { if has {
return ci, nil return ci, nil
} }
db := database.FromCtx(ctx) sRes, err := s.getJSONB(ctx, name)
if err != nil && errors.Is(err, ErrNoSetting) {
cBytes, err := db.GetSetting(ctx, name)
if err != nil {
if database.IsNoRows(err) {
def, hasDefault := s.defaults[name] def, hasDefault := s.defaults[name]
if hasDefault { if hasDefault {
return def, nil return def, nil
} }
return nil, ErrNoSetting
}
return nil, err return nil, err
} }
err = json.Unmarshal(cBytes, ci) err = json.Unmarshal(sRes, &ci)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -98,6 +122,40 @@ func (s *postgresStore) Get(ctx context.Context, name string) (Setting, error) {
return ci, nil 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 { 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)) subj, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceSetting), rbac.WithActions(entities.ActionCreate, entities.ActionUpdate))
if err != nil { if err != nil {