Initial settings
This commit is contained in:
parent
b275881697
commit
c6ca856635
6 changed files with 219 additions and 107 deletions
|
@ -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
128
pkg/rest/prefs.go
Normal 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)
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Reference in a new issue