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
|
||||
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
|
||||
}
|
||||
|
|
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
|
||||
|
||||
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) {
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue