~evanj/cms

398a8759a304b7c11624c9f3642c3c6c07d2ab79 — Evan M Jones 9 months ago ce1376f
WIP(rbac): Large DB interface refactor. Prep for rbac work. TODO: All
tests have broke. Fix them.
M internal/c/content/content.go => internal/c/content/content.go +21 -21
@@ 44,12 44,12 @@ type DBer interface {
	UserGet(username, password string) (user.User, error)
	UserGetFromToken(token string) (user.User, error)
	SpaceGet(user user.User, spaceID string) (space.Space, error)
	ContentTypeGet(space space.Space, contenttypeID string) (contenttype.ContentType, error)
	ContentNew(space space.Space, ct contenttype.ContentType, params []db.ContentNewParam) (content.Content, error)
	ContentGet(space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error)
	ContentUpdate(space space.Space, ct contenttype.ContentType, content content.Content, newParams []db.ContentNewParam, updateParams []db.ContentUpdateParam) (content.Content, error)
	ContentDelete(space space.Space, ct contenttype.ContentType, content content.Content) error
	ContentSearch(space space.Space, ct contenttype.ContentType, name, query string, before int) (content.ContentList, error)
	ContentTypeGet(u user.User, space space.Space, contenttypeID string) (contenttype.ContentType, error)
	ContentNew(u user.User, space space.Space, ct contenttype.ContentType, params []db.ContentNewParam) (content.Content, error)
	ContentGet(u user.User, space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error)
	ContentUpdate(u user.User, space space.Space, ct contenttype.ContentType, content content.Content, newParams []db.ContentNewParam, updateParams []db.ContentUpdateParam) (content.Content, error)
	ContentDelete(u user.User, space space.Space, ct contenttype.ContentType, content content.Content) error
	ContentSearch(u user.User, space space.Space, ct contenttype.ContentType, name, query string, before int) (content.ContentList, error)
}

type E3er interface {


@@ 57,7 57,7 @@ type E3er interface {
}

type Hooker interface {
	Do(space space.Space, content content.Content, ht webhook.HookType)
	Do(user user.User, space space.Space, content content.Content, ht webhook.HookType)
}

func New(c *c.Controller, log *log.Logger, db DBer, e3 E3er, hook Hooker, baseURL string) *Content {


@@ 96,12 96,12 @@ func (c *Content) tree(w http.ResponseWriter, r *http.Request, spaceID, contentt
		return nil, nil, nil, nil, ErrNoSpace
	}

	ct, err := c.db.ContentTypeGet(space, contenttypeID)
	ct, err := c.db.ContentTypeGet(user, space, contenttypeID)
	if err != nil {
		return nil, nil, nil, nil, ErrNoCT
	}

	content, err := c.db.ContentGet(space, ct, contentID)
	content, err := c.db.ContentGet(user, space, ct, contentID)
	if err != nil {
		return nil, nil, nil, nil, ErrNoC
	}


@@ 125,7 125,7 @@ func (c *Content) create(w http.ResponseWriter, r *http.Request) {
		return
	}

	ct, err := c.db.ContentTypeGet(space, contenttypeID)
	ct, err := c.db.ContentTypeGet(user, space, contenttypeID)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, ErrNoCT)
		return


@@ 187,13 187,13 @@ func (c *Content) create(w http.ResponseWriter, r *http.Request) {
		}
	}

	content, err := c.db.ContentNew(space, ct, params)
	content, err := c.db.ContentNew(user, space, ct, params)
	if err != nil {
		c.Error2(w, r, http.StatusInternalServerError, err)
		return
	}

	go c.hook.Do(space, content, webhook.HookNew)
	go c.hook.Do(user, space, content, webhook.HookNew)

	url := fmt.Sprintf("/content/%s/%s/%s", space.ID(), ct.ID(), content.ID())
	c.Redirect(w, r, url)


@@ 223,13 223,13 @@ func (c *Content) serve(w http.ResponseWriter, r *http.Request) {
		return
	}

	ct, err := c.db.ContentTypeGet(space, contenttypeID)
	ct, err := c.db.ContentTypeGet(user, space, contenttypeID)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, ErrNoCT)
		return
	}

	content, err := c.db.ContentGet(space, ct, contentID)
	content, err := c.db.ContentGet(user, space, ct, contentID)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, ErrNoC)
		return


@@ 361,13 361,13 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {
		}
	}

	content, err = c.db.ContentUpdate(space, ct, content, newParams, updateParams)
	content, err = c.db.ContentUpdate(user, space, ct, content, newParams, updateParams)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to update content")
		return
	}

	go c.hook.Do(space, content, webhook.HookUpdate)
	go c.hook.Do(user, space, content, webhook.HookUpdate)

	url := fmt.Sprintf("/content/%s/%s/%s", space.ID(), ct.ID(), content.ID())
	c.Redirect(w, r, url)


@@ 378,18 378,18 @@ func (c *Content) delete(w http.ResponseWriter, r *http.Request) {
	contenttypeID := r.FormValue("contenttype")
	contentID := r.FormValue("content")

	_, space, ct, content, err := c.tree(w, r, spaceID, contenttypeID, contentID)
	user, space, ct, content, err := c.tree(w, r, spaceID, contenttypeID, contentID)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, err)
		return
	}

	if err := c.db.ContentDelete(space, ct, content); err != nil {
	if err := c.db.ContentDelete(user, space, ct, content); err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to delete content")
		return
	}

	go c.hook.Do(space, content, webhook.HookDelete)
	go c.hook.Do(user, space, content, webhook.HookDelete)

	url := fmt.Sprintf("/contenttype/%s/%s", space.ID(), ct.ID())
	c.Redirect(w, r, url)


@@ 417,14 417,14 @@ func (c *Content) search(w http.ResponseWriter, r *http.Request) {
		return
	}

	ct, err := c.db.ContentTypeGet(space, contenttypeID)
	ct, err := c.db.ContentTypeGet(user, space, contenttypeID)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, ErrNoCT)
		return
	}

	before, _ := strconv.Atoi(r.URL.Query().Get("before"))
	list, err := c.db.ContentSearch(space, ct, field, query, before)
	list, err := c.db.ContentSearch(user, space, ct, field, query, before)
	if err != nil {
		return
	}

M internal/c/contenttype/contenttype.go => internal/c/contenttype/contenttype.go +17 -17
@@ 43,12 43,12 @@ type dber interface {
	UserGet(username, password string) (user.User, error)
	UserGetFromToken(token string) (user.User, error)
	SpaceGet(user user.User, spaceID string) (space.Space, error)
	ContentTypeNew(space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error)
	ContentTypeGet(space space.Space, contenttypeID string) (contenttype.ContentType, error)
	ContentTypeUpdate(space space.Space, contenttype contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error)
	ContentTypeDelete(space space.Space, ct contenttype.ContentType) error
	ContentTypeSearch(space space.Space, query string, before int) (contenttype.ContentTypeList, error)
	ContentPerContentType(space space.Space, ct contenttype.ContentType, before int, order db.OrderType, sortField string) (content.ContentList, error)
	ContentTypeNew(user user.User, space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error)
	ContentTypeGet(user user.User, space space.Space, contenttypeID string) (contenttype.ContentType, error)
	ContentTypeUpdate(user user.User, space space.Space, contenttype contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error)
	ContentTypeDelete(user user.User, space space.Space, ct contenttype.ContentType) error
	ContentTypeSearch(user user.User, space space.Space, query string, before int) (contenttype.ContentTypeList, error)
	ContentPerContentType(user user.User, space space.Space, ct contenttype.ContentType, before int, order db.OrderType, sortField string) (content.ContentList, error)
}

func New(c *c.Controller, log *log.Logger, db dber) *ContentType {


@@ 73,7 73,7 @@ func (c *ContentType) tree(w http.ResponseWriter, r *http.Request) (user.User, s
func (ct *ContentType) create(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")

	_, space, err := ct.tree(w, r)
	user, space, err := ct.tree(w, r)
	if err != nil {
		ct.Error2(w, r, http.StatusBadRequest, err)
		return


@@ 121,7 121,7 @@ func (ct *ContentType) create(w http.ResponseWriter, r *http.Request) {
		return
	}

	ctype, err := ct.db.ContentTypeNew(space, name, params)
	ctype, err := ct.db.ContentTypeNew(user, space, name, params)
	if err != nil {
		ct.Error2(w, r, http.StatusInternalServerError, fmt.Errorf("%s: %w", ErrFailedCreate.Error(), err))
		return


@@ 135,13 135,13 @@ func (ct *ContentType) update(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")
	ctID := r.FormValue("contenttype")

	_, space, err := ct.tree(w, r)
	user, space, err := ct.tree(w, r)
	if err != nil {
		ct.Error2(w, r, http.StatusBadRequest, err)
		return
	}

	old, err := ct.db.ContentTypeGet(space, ctID)
	old, err := ct.db.ContentTypeGet(user, space, ctID)
	if err != nil {
		ct.Error2(w, r, http.StatusInternalServerError, ErrNoCT)
		return


@@ 223,13 223,13 @@ func (ct *ContentType) update(w http.ResponseWriter, r *http.Request) {
		return
	}

	ctype, err := ct.db.ContentTypeGet(space, ctID)
	ctype, err := ct.db.ContentTypeGet(user, space, ctID)
	if err != nil {
		ct.Error2(w, r, http.StatusInternalServerError, ErrNoCT)
		return
	}

	ctype, err = ct.db.ContentTypeUpdate(space, ctype, name, newParams, updateParams)
	ctype, err = ct.db.ContentTypeUpdate(user, space, ctype, name, newParams, updateParams)
	if err != nil {
		ct.Error2(w, r, http.StatusInternalServerError, fmt.Errorf("%s: %w", ErrFailedUpdate.Error(), err))
		return


@@ 261,7 261,7 @@ func (c *ContentType) serve(w http.ResponseWriter, r *http.Request) {
		return
	}

	ct, err := c.db.ContentTypeGet(space, contenttypeID)
	ct, err := c.db.ContentTypeGet(user, space, contenttypeID)
	if err != nil {
		c.Error2(w, r, http.StatusInternalServerError, ErrNoCT)
		return


@@ 288,7 288,7 @@ func (c *ContentType) serve(w http.ResponseWriter, r *http.Request) {
	}

	before, _ := strconv.Atoi(r.URL.Query().Get("before"))
	list, err := c.db.ContentPerContentType(space, ct, before, o, f)
	list, err := c.db.ContentPerContentType(user, space, ct, before, o, f)
	if err != nil {
		c.Error2(w, r, http.StatusInternalServerError, ErrNoC)
		return


@@ 318,13 318,13 @@ func (c *ContentType) delete(w http.ResponseWriter, r *http.Request) {
		return
	}

	ct, err := c.db.ContentTypeGet(space, contenttypeID)
	ct, err := c.db.ContentTypeGet(user, space, contenttypeID)
	if err != nil {
		c.Error2(w, r, http.StatusInternalServerError, ErrNoCT)
		return
	}

	if err := c.db.ContentTypeDelete(space, ct); err != nil {
	if err := c.db.ContentTypeDelete(user, space, ct); err != nil {
		c.Error2(w, r, http.StatusInternalServerError, ErrFailedDelete)
		return
	}


@@ 350,7 350,7 @@ func (c *ContentType) search(w http.ResponseWriter, r *http.Request) {
	}

	before, _ := strconv.Atoi(r.URL.Query().Get("before"))
	list, err := c.db.ContentTypeSearch(space, query, before)
	list, err := c.db.ContentTypeSearch(user, space, query, before)
	if err != nil {
		c.Error2(w, r, http.StatusInternalServerError, ErrNoCT)
		return

M internal/c/hook/hook.go => internal/c/hook/hook.go +7 -7
@@ 33,9 33,9 @@ type dber interface {
	UserGet(username, password string) (user.User, error)
	UserGetFromToken(token string) (user.User, error)
	SpaceGet(user user.User, spaceID string) (space.Space, error)
	HookNew(space space.Space, url string) (hook.Hook, error)
	HookGet(space space.Space, id string) (hook.Hook, error)
	HookDelete(space space.Space, hook hook.Hook) error
	HookNew(user user.User, space space.Space, url string) (hook.Hook, error)
	HookGet(user user.User, space space.Space, id string) (hook.Hook, error)
	HookDelete(user user.User, space space.Space, hook hook.Hook) error
}

func New(c *c.Controller, log *log.Logger, db dber) *Content {


@@ 62,7 62,7 @@ func (h *Content) ServeHTTP(w http.ResponseWriter, r *http.Request) {
			return
		}

		hook, err := h.db.HookNew(space, r.FormValue("url"))
		hook, err := h.db.HookNew(user, space, r.FormValue("url"))
		if err != nil {
			h.Error2(w, r, http.StatusInternalServerError, ErrFailedCreate)
			return


@@ 78,13 78,13 @@ func (h *Content) ServeHTTP(w http.ResponseWriter, r *http.Request) {
			return
		}

		hook, err := h.db.HookGet(space, r.FormValue("hook"))
		hook, err := h.db.HookGet(user, space, r.FormValue("hook"))
		if err != nil {
			h.Error2(w, r, http.StatusBadRequest, ErrNoHook)
			return
		}

		if err := h.db.HookDelete(space, hook); err != nil {
		if err := h.db.HookDelete(user, space, hook); err != nil {
			h.Error2(w, r, http.StatusInternalServerError, ErrFailedDelete)
			return
		}


@@ 108,7 108,7 @@ func (h *Content) ServeHTTP(w http.ResponseWriter, r *http.Request) {
			return
		}

		hook, err := h.db.HookGet(space, hookID)
		hook, err := h.db.HookGet(user, space, hookID)
		if err != nil {
			h.Error2(w, r, http.StatusBadRequest, ErrNoHook)
			return

M internal/c/space/space.go => internal/c/space/space.go +4 -4
@@ 37,8 37,8 @@ type dber interface {
	SpaceUpdate(user user.User, space space.Space, name, desc string) (space.Space, error)
	SpaceCopy(user user.User, space space.Space, name, desc string) (space.Space, error)
	SpaceDelete(user user.User, space space.Space) error
	ContentTypesPerSpace(space space.Space, before int) (contenttype.ContentTypeList, error)
	HooksPerSpace(space space.Space, before int) (hook.HookList, error)
	ContentTypesPerSpace(user user.User, space space.Space, before int) (contenttype.ContentTypeList, error)
	HooksPerSpace(user user.User, space space.Space, before int) (hook.HookList, error)
}

func New(c *c.Controller, log *log.Logger, db dber) *Space {


@@ 69,14 69,14 @@ func (s *Space) serve(w http.ResponseWriter, r *http.Request) {
	}

	beforect, _ := strconv.Atoi(r.URL.Query().Get("beforect"))
	cts, err := s.db.ContentTypesPerSpace(space, beforect)
	cts, err := s.db.ContentTypesPerSpace(user, space, beforect)
	if err != nil {
		s.Error(w, r, http.StatusInternalServerError, "failed to find contenttypes for space")
		return
	}

	beforehook, _ := strconv.Atoi(r.URL.Query().Get("beforehook"))
	hooks, err := s.db.HooksPerSpace(space, beforehook)
	hooks, err := s.db.HooksPerSpace(user, space, beforehook)
	if err != nil {
		s.Error(w, r, http.StatusInternalServerError, "failed to find webhooks for space")
		return

M internal/s/cache/content.go => internal/s/cache/content.go +11 -8
@@ 7,6 7,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"github.com/bradfitz/gomemcache/memcache"
)


@@ 19,8 20,8 @@ func (c *Cache) content(breakCache bool, key string, getter func() (content.Cont
	return v, nil
}

func (c *Cache) ContentNew(space space.Space, ct contenttype.ContentType, params []db.ContentNewParam) (content.Content, error) {
	thing, err := c.db.ContentNew(space, ct, params)
func (c *Cache) ContentNew(u user.User, space space.Space, ct contenttype.ContentType, params []db.ContentNewParam) (content.Content, error) {
	thing, err := c.db.ContentNew(u, space, ct, params)
	if err != nil {
		return nil, err
	}


@@ 32,19 33,21 @@ func (c *Cache) ContentNew(space space.Space, ct contenttype.ContentType, params
	)
}

func (c *Cache) ContentGet(space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error) {
func (c *Cache) ContentGet(u user.User, space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error) {
	return c.content(
		false,
		fmt.Sprintf("content::%s::%s::%s::%s", c.baseKey, space.ID(), ct.ID(), contentID),
		func() (content.Content, error) { return c.db.ContentGet(space, ct, contentID) },
		func() (content.Content, error) { return c.db.ContentGet(u, space, ct, contentID) },
	)
}

func (c *Cache) ContentUpdate(space space.Space, ct contenttype.ContentType, item content.Content, newParams []db.ContentNewParam, updateParams []db.ContentUpdateParam) (content.Content, error) {
func (c *Cache) ContentUpdate(u user.User, space space.Space, ct contenttype.ContentType, item content.Content, newParams []db.ContentNewParam, updateParams []db.ContentUpdateParam) (content.Content, error) {
	content, err := c.content(
		true,
		fmt.Sprintf("content::%s::%s::%s::%s", c.baseKey, space.ID(), ct.ID(), item.ID()),
		func() (content.Content, error) { return c.db.ContentUpdate(space, ct, item, newParams, updateParams) },
		func() (content.Content, error) {
			return c.db.ContentUpdate(u, space, ct, item, newParams, updateParams)
		},
	)
	if err != nil {
		return nil, err


@@ 66,7 69,7 @@ func (c *Cache) ContentUpdate(space space.Space, ct contenttype.ContentType, ite
	return content, nil
}

func (c *Cache) ContentDelete(space space.Space, ct contenttype.ContentType, item content.Content) error {
func (c *Cache) ContentDelete(u user.User, space space.Space, ct contenttype.ContentType, item content.Content) error {
	key := fmt.Sprintf("content::%s::%s::%s::%s", c.baseKey, space.ID(), ct.ID(), item.ID())

	list, err := c.db.ContentRefererList(item.ID())


@@ 79,7 82,7 @@ func (c *Cache) ContentDelete(space space.Space, ct contenttype.ContentType, ite
		true,
		key,
		func() (content.Content, error) {
			deleteErr = c.db.ContentDelete(space, ct, item)
			deleteErr = c.db.ContentDelete(u, space, ct, item)
			return nil, deleteErr
		},
	)

M internal/s/cache/contenttype.go => internal/s/cache/contenttype.go +11 -10
@@ 5,6 5,7 @@ import (

	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"github.com/bradfitz/gomemcache/memcache"
	"github.com/pkg/errors"


@@ 23,8 24,8 @@ func (c *Cache) contenttype(breakCache bool, key string, getter func() (contentt
	return v, nil
}

func (c *Cache) ContentTypeNew(space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error) {
	thing, err := c.db.ContentTypeNew(space, name, params)
func (c *Cache) ContentTypeNew(u user.User, space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error) {
	thing, err := c.db.ContentTypeNew(u, space, name, params)
	if err != nil {
		return nil, err
	}


@@ 36,13 37,13 @@ func (c *Cache) ContentTypeNew(space space.Space, name string, params []db.Conte
	)
}

func (c *Cache) ContentTypeUpdate(space space.Space, prev contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error) {
	next, err := c.db.ContentTypeUpdate(space, prev, name, newParams, updateParams)
func (c *Cache) ContentTypeUpdate(u user.User, space space.Space, prev contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error) {
	next, err := c.db.ContentTypeUpdate(u, space, prev, name, newParams, updateParams)
	if err != nil {
		return nil, err
	}

	iter, t, err := c.db.ContentIter(space, next, "name") // TODO: What is this sort type for?
	iter, t, err := c.db.ContentIter(u, space, next, "name") // TODO: What is this sort type for?
	if err != nil {
		return nil, errors.Wrap(err, "failed to iterate content for cache breaking")
	}


@@ 72,19 73,19 @@ func (c *Cache) ContentTypeUpdate(space space.Space, prev contenttype.ContentTyp
	)
}

func (c *Cache) ContentTypeGet(space space.Space, thingID string) (contenttype.ContentType, error) {
func (c *Cache) ContentTypeGet(u user.User, space space.Space, thingID string) (contenttype.ContentType, error) {
	return c.contenttype(
		false,
		fmt.Sprintf("contenttype::%s::%s::%s", c.baseKey, space.ID(), thingID),
		func() (contenttype.ContentType, error) { return c.db.ContentTypeGet(space, thingID) },
		func() (contenttype.ContentType, error) { return c.db.ContentTypeGet(u, space, thingID) },
	)
}

func (c *Cache) ContentTypeDelete(space space.Space, ct contenttype.ContentType) error {
func (c *Cache) ContentTypeDelete(u user.User, space space.Space, ct contenttype.ContentType) error {
	key := fmt.Sprintf("contenttype::%s::%s::%s", c.baseKey, space.ID(), ct.ID())

	// Copy all contents and their values.
	iter, t, err := c.db.ContentIter(space, ct, "name") // TODO: What is this sort type for?
	iter, t, err := c.db.ContentIter(u, space, ct, "name") // TODO: What is this sort type for?
	if err != nil {
		return errors.Wrap(err, "failed to iterate referring content for cache breaking")
	}


@@ 114,7 115,7 @@ func (c *Cache) ContentTypeDelete(space space.Space, ct contenttype.ContentType)
		true,
		key,
		func() (contenttype.ContentType, error) {
			deleteErr = c.db.ContentTypeDelete(space, ct)
			deleteErr = c.db.ContentTypeDelete(u, space, ct)
			return nil, deleteErr
		},
	)

M internal/s/db/content.go => internal/s/db/content.go +18 -16
@@ 10,6 10,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/m/value"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
	"github.com/google/uuid"


@@ 512,7 513,7 @@ func (db *DB) contentNew(t *sql.Tx, space space.Space, ct contenttype.ContentTyp
	return &content, nil
}

func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params []ContentNewParam) (content.Content, error) {
func (db *DB) ContentNew(u user.User, space space.Space, ct contenttype.ContentType, params []ContentNewParam) (content.Content, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err


@@ 706,7 707,7 @@ func (db *DB) contentUpdate(t *sql.Tx, space space.Space, ct contenttype.Content
	return nil
}

func (db *DB) ContentUpdate(space space.Space, ct contenttype.ContentType, content content.Content, newParams []ContentNewParam, updateParams []ContentUpdateParam) (content.Content, error) {
func (db *DB) ContentUpdate(u user.User, space space.Space, ct contenttype.ContentType, content content.Content, newParams []ContentNewParam, updateParams []ContentUpdateParam) (content.Content, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err


@@ 721,10 722,10 @@ func (db *DB) ContentUpdate(space space.Space, ct contenttype.ContentType, conte
		return nil, err
	}

	return db.ContentGet(space, ct, content.ID())
	return db.ContentGet(u, space, ct, content.ID())
}

func (db *DB) ContentDelete(space space.Space, ct contenttype.ContentType, content content.Content) error {
func (db *DB) ContentDelete(u user.User, space space.Space, ct contenttype.ContentType, content content.Content) error {
	t, err := db.Begin()
	if err != nil {
		return err


@@ 785,7 786,7 @@ func sortinfo(t *sql.Tx, ct contenttype.ContentType, sortField string) (string, 
	return sortFieldValueType, sortFieldTableName, nil
}

func (db *DB) contentPerContentType(t *sql.Tx, space space.Space, ct contenttype.ContentType, before int, order OrderType, sortField string, depth int) (content.ContentList, error) {
func (db *DB) contentPerContentType(t *sql.Tx, u user.User, space space.Space, ct contenttype.ContentType, before int, order OrderType, sortField string, depth int) (content.ContentList, error) {
	var (
		tmpID        int
		tmpContentID string


@@ 858,7 859,7 @@ func (db *DB) contentPerContentType(t *sql.Tx, space space.Space, ct contenttype
			return nil, err
		}

		c, err := db.ContentGet(space, ct, tmpContentID)
		c, err := db.ContentGet(u, space, ct, tmpContentID)
		if err != nil {
			return nil, err
		}


@@ 869,14 870,14 @@ func (db *DB) contentPerContentType(t *sql.Tx, space space.Space, ct contenttype
	return newContentList(r, hasMore, tmpID), nil
}

func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentType, before int, order OrderType, sortField string) (content.ContentList, error) {
func (db *DB) ContentPerContentType(u user.User, space space.Space, ct contenttype.ContentType, before int, order OrderType, sortField string) (content.ContentList, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	list, err := db.contentPerContentType(t, space, ct, before, order, sortField, defaultDepth)
	list, err := db.contentPerContentType(t, u, space, ct, before, order, sortField, defaultDepth)
	if err != nil {
		return nil, err
	}


@@ 884,7 885,7 @@ func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentTyp
	return list, t.Commit()
}

func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, sortField, query string, before int) (content.ContentList, error) {
func (db *DB) ContentSearch(u user.User, space space.Space, ct contenttype.ContentType, sortField, query string, before int) (content.ContentList, error) {
	var (
		tmpID        int
		tmpContentID string


@@ 963,7 964,7 @@ func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, sortF
			return nil, err
		}

		c, err := db.ContentGet(space, ct, tmpContentID)
		c, err := db.ContentGet(u, space, ct, tmpContentID)
		if err != nil {
			return nil, err
		}


@@ 1012,7 1013,7 @@ func (db *DB) contentGet(t *sql.Tx, space space.Space, ct contenttype.ContentTyp
	return &content, nil
}

func (db *DB) ContentGet(space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error) {
func (db *DB) ContentGet(u user.User, space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error) {
	if space == nil {
		return nil, fmt.Errorf("must provide parent space")
	}


@@ 1246,6 1247,7 @@ func (c *ContentValue) UnmarshalJSON(b []byte) error {
type contentIter struct {
	db        *DB
	t         *sql.Tx // Not used for the moment (we only use simple queries).
	user      user.User
	space     space.Space
	ct        contenttype.ContentType
	sortField string


@@ 1255,22 1257,22 @@ type contentIter struct {
	err  error
}

func (db *DB) contentIter(t *sql.Tx, space space.Space, ct contenttype.ContentType, sortField string) *contentIter {
	iter := &contentIter{db, t, space, ct, sortField, newContentList(nil, false, 0), nil}
func (db *DB) contentIter(t *sql.Tx, user user.User, space space.Space, ct contenttype.ContentType, sortField string) *contentIter {
	iter := &contentIter{db, t, user, space, ct, sortField, newContentList(nil, false, 0), nil}
	iter.pump()
	return iter
}

func (db *DB) ContentIter(space space.Space, ct contenttype.ContentType, sortField string) (*contentIter, *sql.Tx, error) {
func (db *DB) ContentIter(u user.User, space space.Space, ct contenttype.ContentType, sortField string) (*contentIter, *sql.Tx, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, nil, err
	}
	return db.contentIter(t, space, ct, sortField), t, nil
	return db.contentIter(t, u, space, ct, sortField), t, nil
}

func (iter *contentIter) pump() {
	list, err := iter.db.contentPerContentType(iter.t, iter.space, iter.ct, iter.list.Before(), OrderAsc, iter.sortField, defaultDepth)
	list, err := iter.db.contentPerContentType(iter.t, iter.user, iter.space, iter.ct, iter.list.Before(), OrderAsc, iter.sortField, defaultDepth)
	iter.list = list
	iter.err = err
}

M internal/s/db/contenttype.go => internal/s/db/contenttype.go +10 -9
@@ 8,6 8,7 @@ import (

	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
)



@@ 67,7 68,7 @@ var (
	`
)

func (db *DB) ContentTypeNew(space space.Space, name string, params []ContentTypeNewParam) (contenttype.ContentType, error) {
func (db *DB) ContentTypeNew(u user.User, space space.Space, name string, params []ContentTypeNewParam) (contenttype.ContentType, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err


@@ 117,7 118,7 @@ func (db *DB) ContentTypeNew(space space.Space, name string, params []ContentTyp
// ContentTypeUpdate will either remove fields are add fields to a contenttype.
// Note: field types cannot be changed (e.g. if a field is of type InputHTML it
// cannot become a StringSmall. Field type names can be changed.
func (db *DB) ContentTypeUpdate(space space.Space, contenttype contenttype.ContentType, name string, newParams []ContentTypeNewParam, updateParams []ContentTypeUpdateParam) (contenttype.ContentType, error) {
func (db *DB) ContentTypeUpdate(u user.User, space space.Space, contenttype contenttype.ContentType, name string, newParams []ContentTypeNewParam, updateParams []ContentTypeUpdateParam) (contenttype.ContentType, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err


@@ 164,7 165,7 @@ func (db *DB) ContentTypeUpdate(space space.Space, contenttype contenttype.Conte
		return nil, err
	}

	return db.ContentTypeGet(space, contenttype.ID())
	return db.ContentTypeGet(u, space, contenttype.ID())
}

func (db *DB) contentTypesPerSpace(t *sql.Tx, space space.Space, before int) (contenttype.ContentTypeList, error) {


@@ 209,7 210,7 @@ func (db *DB) contentTypesPerSpace(t *sql.Tx, space space.Space, before int) (co
	return newContentTypeList(r, hasMore, id), nil
}

func (db *DB) ContentTypesPerSpace(space space.Space, before int) (contenttype.ContentTypeList, error) {
func (db *DB) ContentTypesPerSpace(u user.User, space space.Space, before int) (contenttype.ContentTypeList, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err


@@ 245,7 246,7 @@ func (db *DB) contentTypeGet(t *sql.Tx, space space.Space, contenttypeID string)
	return &ct, nil
}

func (db *DB) ContentTypeGet(space space.Space, contenttypeID string) (contenttype.ContentType, error) {
func (db *DB) ContentTypeGet(u user.User, space space.Space, contenttypeID string) (contenttype.ContentType, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err


@@ 262,7 263,7 @@ func (db *DB) ContentTypeGet(space space.Space, contenttypeID string) (contentty

// TODO: Consolidate with other list function here. They are the same except for
// the query used.
func (db *DB) ContentTypeSearch(space space.Space, query string, before int) (contenttype.ContentTypeList, error) {
func (db *DB) ContentTypeSearch(u user.User, space space.Space, query string, before int) (contenttype.ContentTypeList, error) {
	var (
		r       []contenttype.ContentType
		id      int


@@ 284,7 285,7 @@ func (db *DB) ContentTypeSearch(space space.Space, query string, before int) (co
			return nil, err
		}

		ct, err := db.ContentTypeGet(space, strconv.Itoa(id))
		ct, err := db.ContentTypeGet(u, space, strconv.Itoa(id))
		if err != nil {
			return nil, err
		}


@@ 295,7 296,7 @@ func (db *DB) ContentTypeSearch(space space.Space, query string, before int) (co
	return newContentTypeList(r, hasMore, id), nil
}

func (db *DB) ContentTypeDelete(space space.Space, ct contenttype.ContentType) error {
func (db *DB) ContentTypeDelete(u user.User, space space.Space, ct contenttype.ContentType) error {
	t, err := db.Begin()
	if err != nil {
		return err


@@ 363,7 364,7 @@ type contentTypeIter struct {
	err  error
}

func (db *DB) ContentTypeIter(t *sql.Tx, space space.Space) *contentTypeIter {
func (db *DB) ContentTypeIter(t *sql.Tx, u user.User, space space.Space) *contentTypeIter {
	iter := &contentTypeIter{db, t, space, newContentTypeList(nil, false, 0), nil}
	iter.pump()
	return iter

M internal/s/db/hook.go => internal/s/db/hook.go +6 -5
@@ 6,6 6,7 @@ import (

	"git.sr.ht/~evanj/cms/internal/m/hook"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/user"
)

const (


@@ 30,7 31,7 @@ type Hook struct {
	id, url, spaceID string
}

func (db *DB) HookNew(space space.Space, url string) (hook.Hook, error) {
func (db *DB) HookNew(u user.User, space space.Space, url string) (hook.Hook, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err


@@ 64,7 65,7 @@ func (db *DB) hookGet(t *sql.Tx, space space.Space, id string) (hook.Hook, error
	return &hook, nil
}

func (db *DB) HookGet(space space.Space, id string) (hook.Hook, error) {
func (db *DB) HookGet(u user.User, space space.Space, id string) (hook.Hook, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err


@@ 79,7 80,7 @@ func (db *DB) HookGet(space space.Space, id string) (hook.Hook, error) {
	return h, t.Commit()
}

func (db *DB) HookDelete(s space.Space, h hook.Hook) error {
func (db *DB) HookDelete(u user.User, s space.Space, h hook.Hook) error {
	t, err := db.Begin()
	if err != nil {
		return err


@@ 134,7 135,7 @@ func (db *DB) hooksPerSpace(t *sql.Tx, space space.Space, before int) (hook.Hook
	return newHookList(r, hasMore, id), nil
}

func (db *DB) HooksPerSpace(space space.Space, before int) (hook.HookList, error) {
func (db *DB) HooksPerSpace(u user.User, space space.Space, before int) (hook.HookList, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err


@@ 166,7 167,7 @@ type hookIter struct {
	err  error
}

func (db *DB) HookIter(t *sql.Tx, space space.Space) *hookIter {
func (db *DB) HookIter(t *sql.Tx, u user.User, space space.Space) *hookIter {
	iter := &hookIter{db, t, space, newHookList(nil, false, 0), nil}
	iter.pump()
	return iter

M internal/s/db/invite.go => internal/s/db/invite.go +2 -1
@@ 163,7 163,8 @@ func (db *DB) InviteList(o org.Org) (r []invite.Invite, err error) {
	var (
		now  = time.Now().UTC()
		from = now.Format(mysqlTimeLayout)
		to   = now.Add(1 * time.Hour).Format(mysqlTimeLayout)
		// Add a second here. Offset it slightly so user receives expected results.
		to = now.Add(1 * time.Hour).Add(1 * time.Second).Format(mysqlTimeLayout)
	)

	rows, err := db.Query(queryList, o.ID(), to, from)

M internal/s/db/space.go => internal/s/db/space.go +4 -4
@@ 152,14 152,14 @@ func (db *DB) SpaceCopy(user user.User, prevS space.Space, name, desc string) (s
		return nil, err
	}

	if err := db.spaceCopyContentTypes(t, next, prevS); err != nil {
	if err := db.spaceCopyContentTypes(t, user, next, prevS); err != nil {
		return nil, err
	}

	return next, t.Commit()
}

func (db *DB) spaceCopyContentTypes(t *sql.Tx, next, prevS space.Space) error {
func (db *DB) spaceCopyContentTypes(t *sql.Tx, u user.User, next, prevS space.Space) error {
	type cct struct {
		ct contenttype.ContentType
		c  content.Content


@@ 179,7 179,7 @@ func (db *DB) spaceCopyContentTypes(t *sql.Tx, next, prevS space.Space) error {

	// Copy all content types and their value types.
	// Copy all contents and their values.
	iter := db.ContentTypeIter(t, prevS)
	iter := db.ContentTypeIter(t, u, prevS)
	for iter.Next() {
		prevCT, err := iter.Scan()
		if err != nil {


@@ 208,7 208,7 @@ func (db *DB) spaceCopyContentTypes(t *sql.Tx, next, prevS space.Space) error {
		}

		// Copy all contents and their values.
		iter := db.contentIter(t, prevS, prevCT, "name") // TODO: What is this sort type for?
		iter := db.contentIter(t, u, prevS, prevCT, "name") // TODO: What is this sort type for?
		for iter.Next() {
			prevC, err := iter.Scan()
			if err != nil {

M internal/s/hook/hook.go => internal/s/hook/hook.go +4 -3
@@ 14,6 14,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/hook"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"golang.org/x/sync/errgroup"
)



@@ 32,7 33,7 @@ type Hook struct {
}

type dber interface {
	HooksPerSpace(space space.Space, before int) (hook.HookList, error)
	HooksPerSpace(user user.User, space space.Space, before int) (hook.HookList, error)
}

func New(log *log.Logger, db dber) *Hook {


@@ 77,7 78,7 @@ func (h *Hook) do(ctx context.Context, content content.Content, hook hook.Hook, 
	return nil
}

func (h *Hook) Do(space space.Space, content content.Content, ht HookType) {
func (h *Hook) Do(user user.User, space space.Space, content content.Content, ht HookType) {
	var (
		hooks       hook.HookList
		before      int


@@ 91,7 92,7 @@ func (h *Hook) Do(space space.Space, content content.Content, ht HookType) {
	defer cancel()

	for i := 0; i == 0 || hooks.More(); i++ {
		hooks, err = h.db.HooksPerSpace(space, before)
		hooks, err = h.db.HooksPerSpace(user, space, before)
		if err != nil {
			h.log.Println("failed to find webhooks for", space.ID(), space.Name(), err)
			return

M internal/s/rbac/rbac.go => internal/s/rbac/rbac.go +4 -0
@@ 11,3 11,7 @@ type RBAC struct {
	log *log.Logger
	db  rl.RL // Or DB or Cache, depends on DI order in main.
}

func New(l *log.Logger, db rl.RL) RBAC {
	return RBAC{db, l, db}
}

M internal/s/rl/rl.go => internal/s/rl/rl.go +4 -4
@@ 160,18 160,18 @@ func updateParamHasFile(params []db.ContentTypeUpdateParam) (r bool) {
	return
}

func (rl RL) ContentTypeNew(space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error) {
func (rl RL) ContentTypeNew(u user.User, space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error) {
	if space.Org().Tier().Is(tier.Free) && newParamHasFile(params) {
		return nil, fmt.Errorf("can't create content type with field type of file: %w", ErrNoAccess)
	}
	return rl.db.ContentTypeNew(space, name, params)
	return rl.db.ContentTypeNew(u, space, name, params)
}

func (rl RL) ContentTypeUpdate(space space.Space, contenttype contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error) {
func (rl RL) ContentTypeUpdate(u user.User, space space.Space, contenttype contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error) {
	if space.Org().Tier().Is(tier.Free) && (newParamHasFile(newParams) || updateParamHasFile(updateParams)) {
		return nil, fmt.Errorf("can't create content type with field type of file: %w", ErrNoAccess)
	}
	return rl.db.ContentTypeUpdate(space, contenttype, name, newParams, updateParams)
	return rl.db.ContentTypeUpdate(u, space, contenttype, name, newParams, updateParams)
}

// Rate limit users to org.

M main.go => main.go +15 -13
@@ 20,6 20,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/s/cache"
	"git.sr.ht/~evanj/cms/internal/s/db"
	webhook "git.sr.ht/~evanj/cms/internal/s/hook"
	"git.sr.ht/~evanj/cms/internal/s/rbac"
	"git.sr.ht/~evanj/cms/internal/s/rl"
	libstripe "git.sr.ht/~evanj/cms/internal/s/stripe"
	"git.sr.ht/~evanj/cms/pkg/e3"


@@ 71,8 72,9 @@ func main() {

		fs   = e3.New(e3user, e3pass, e3url)
		rl   = rl.New(log.New(w, "[cms:ratelimit] ", 0), cacher, fs)
		c    = c.New(log.New(w, "[cms:content] ", 0), rl, analyticsEnabled, build)
		libs = libstripe.New(log.New(w, "[cms:stripe] ", 0), stripeSuccessURL, stripeErrorURL, stripePK, stripeSK, rl)
		rbac = rbac.New(log.New(w, "[cms:rbac] ", 0), rl)
		c    = c.New(log.New(w, "[cms:content] ", 0), rbac, analyticsEnabled, build)
		libs = libstripe.New(log.New(w, "[cms:stripe] ", 0), stripeSuccessURL, stripeErrorURL, stripePK, stripeSK, rbac)

		app = &App{
			applogger,


@@ 80,37 82,37 @@ func main() {
				"content": content.New(
					c,
					log.New(w, "[cms:content] ", 0),
					rl,
					rbac,
					fs,
					webhook.New(log.New(w, "[cms:hook] ", 0), rl),
					webhook.New(log.New(w, "[cms:hook] ", 0), rbac),
					url,
				),
				"contenttype": contenttype.New(
					c,
					log.New(w, "[cms:contenttype] ", 0),
					rl,
					rbac,
				),
				"space": space.New(
					c,
					log.New(w, "[cms:space] ", 0),
					rl,
					rbac,
				),
				"user": user.New(
					c,
					log.New(w, "[cms:user] ", 0),
					rl,
					rbac,
					signupEnabled,
					libs,
				),
				"hook": hook.New(
					c,
					log.New(w, "[cms:hook] ", 0),
					rl,
					rbac,
				),
				"file": file.New(
					c,
					log.New(w, "[cms:file] ", 0),
					rl,
					rbac,
					fs,
					url,
				),


@@ 118,23 120,23 @@ func main() {
				"redirect": redirect.New(
					c,
					log.New(w, "[cms:redirect] ", 0),
					rl,
					rbac,
				),
				"page": doc.New(
					c,
					log.New(w, "[cms:doc] ", 0),
					rl,
					rbac,
				),
				"stripe": http.StripPrefix("/stripe", stripe.New(
					c,
					log.New(w, "[cms:stripe] ", 0),
					rl,
					rbac,
					libs,
				)),
				"invite": http.StripPrefix("/invite", invite.New(
					c,
					log.New(w, "[cms:doc] ", 0),
					rl,
					rbac,
				)),
			},
		}

M makefile => makefile +1 -1
@@ 32,4 32,4 @@ run: gen build
	@env $(ENV) ./$(BIN)

dev:
	@find * -not -name '*_embed.go' | grep -E '*.(sql|go|js|css|html)' | entr -r make run
	@find * -not -name '*_embed.go' | grep -E '*.(sql|go|js|css|html)' | entr -cr make run