~evanj/cms

5eb977545bd0bfaacf289952a8b7f5a273168ab6 — Evan M Jones a month ago 0ac6989
Chore(Error2 -> Error): Swapped Error impl. for Error2 in all
controllers.
M TODO => TODO +1 -3
@@ 2,14 2,12 @@
Testing: 100% happy path and 80% total
Documentation
Doc pages: Contact, FAQ, Terms, Privacy, Tour
Roll based access control
Depth option on APIs
When editing existing references don't blow away prev inputs
Forgot password
Warn & delete excess users/spaces on downgrade.
Cache: org?, invite?, hook
Cache: org?, invite?, role?, hook
Admin: change role level of users.
Invite: specify role level.

[med]
Cache lists

M internal/c/c.go => internal/c/c.go +3 -7
@@ 120,11 120,7 @@ func (c *Controller) ErrorString(w http.ResponseWriter, r *http.Request, code in
	fmt.Fprintf(w, str)
}

func (c *Controller) Error(w http.ResponseWriter, r *http.Request, code int, str string) {
	c.ErrorString(w, r, code, str)
}

func (c *Controller) Error2(w http.ResponseWriter, r *http.Request, code int, err error) {
func (c *Controller) Error(w http.ResponseWriter, r *http.Request, code int, err error) {
	w.Header().Add("Content-Type", "text/plain")
	w.WriteHeader(code)
	fmt.Fprintf(w, err.Error())


@@ 153,7 149,7 @@ func (c *Controller) HTML(w http.ResponseWriter, r *http.Request, tmpl *template

	if err := tmpl.Execute(&buf, data); err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to build html response")
		c.Error(w, r, http.StatusInternalServerError, errors.New("failed to build html response"))
		return
	}



@@ 169,7 165,7 @@ func (c *Controller) HTML(w http.ResponseWriter, r *http.Request, tmpl *template
func (c *Controller) JSON(w http.ResponseWriter, r *http.Request, data interface{}) {
	bytes, err := json.Marshal(data)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to build json response")
		c.Error(w, r, http.StatusInternalServerError, errors.New("failed to build json response"))
		return
	}


M internal/c/c_test.go => internal/c/c_test.go +2 -2
@@ 285,10 285,10 @@ func (s server5) ServeHTTP(w http.ResponseWriter, r *http.Request) {
		s.ErrorString(w, r, 500+s.typ, s.str)
		break
	case 2:
		s.Error(w, r, 500+s.typ, s.str)
		s.Error(w, r, 500+s.typ, errors.New(s.str))
		break
	case 3:
		s.Error2(w, r, 500+s.typ, errors.New(s.str))
		s.Error(w, r, 500+s.typ, errors.New(s.str))
		break
	}
}

M internal/c/content/content.go => internal/c/content/content.go +26 -26
@@ 2,7 2,6 @@ package content

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"


@@ 20,6 19,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/s/db"
	webhook "git.sr.ht/~evanj/cms/internal/s/hook"
	"git.sr.ht/~evanj/cms/internal/v"
	"github.com/pkg/errors"
)

var (


@@ 115,19 115,19 @@ func (c *Content) create(w http.ResponseWriter, r *http.Request) {

	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, err)
		c.Error(w, r, http.StatusBadRequest, err)
		return
	}

	space, err := c.db.SpaceGet(user, spaceID)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, ErrNoSpace)
		c.Error(w, r, http.StatusBadRequest, errors.Wrap(err, ErrNoSpace.Error()))
		return
	}

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



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

		parts := strings.Split(key, "-")
		if len(parts) < 2 {
			c.Error(w, r, http.StatusInternalServerError, "invalid name field for value")
			c.Error(w, r, http.StatusInternalServerError, errors.New("invalid name field for value"))
			return
		}



@@ 161,19 161,19 @@ func (c *Content) create(w http.ResponseWriter, r *http.Request) {
		for key := range r.MultipartForm.File {
			file, header, err := r.FormFile(key)
			if err != nil {
				c.Error(w, r, http.StatusInternalServerError, "failed to retreive file")
				c.Error(w, r, http.StatusInternalServerError, fmt.Errorf("failed to retreive file: %w", err))
				return
			}

			url, err := c.upload(r.Context(), header.Filename, file, user)
			if err != nil {
				c.Error(w, r, http.StatusInternalServerError, "failed to upload file")
				c.Error(w, r, http.StatusInternalServerError, fmt.Errorf("failed to upload file: %w", err))
				return
			}

			parts := strings.Split(key, "-")
			if len(parts) < 2 {
				c.Error(w, r, http.StatusInternalServerError, "invalid name field for value")
				c.Error(w, r, http.StatusInternalServerError, errors.New("invalid name field for value"))
				return
			}



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

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



@@ 213,25 213,25 @@ func (c *Content) serve(w http.ResponseWriter, r *http.Request) {

	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, err)
		c.Error(w, r, http.StatusBadRequest, err)
		return
	}

	space, err := c.db.SpaceGet(user, spaceID)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, ErrNoSpace)
		c.Error(w, r, http.StatusBadRequest, errors.Wrap(err, ErrNoSpace.Error()))
		return
	}

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

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



@@ 250,7 250,7 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {

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



@@ 270,7 270,7 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {

			parts := strings.Split(strings.ReplaceAll(key, "value_update_", ""), "-")
			if len(parts) < 2 {
				c.Error(w, r, http.StatusInternalServerError, "invalid name field for value")
				c.Error(w, r, http.StatusInternalServerError, errors.New("invalid name field for value"))
				return
			}



@@ 293,7 293,7 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {

		parts := strings.Split(key, "-")
		if len(parts) < 2 {
			c.Error(w, r, http.StatusInternalServerError, "invalid name field for value")
			c.Error(w, r, http.StatusInternalServerError, errors.New("invalid name field for value"))
			return
		}



@@ 315,13 315,13 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {
		for key := range r.MultipartForm.File {
			file, header, err := r.FormFile(key)
			if err != nil {
				c.Error(w, r, http.StatusInternalServerError, "failed to retreive file")
				c.Error(w, r, http.StatusInternalServerError, fmt.Errorf("failed to retreive file: %w", err))
				return
			}

			url, err := c.upload(r.Context(), header.Filename, file, user)
			if err != nil {
				c.Error(w, r, http.StatusInternalServerError, "failed to upload file")
				c.Error(w, r, http.StatusInternalServerError, fmt.Errorf("failed to upload file: %w", err))
				return
			}



@@ 330,7 330,7 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {

				parts := strings.Split(strings.ReplaceAll(key, "value_update_", ""), "-")
				if len(parts) < 2 {
					c.Error(w, r, http.StatusInternalServerError, "invalid name field for value")
					c.Error(w, r, http.StatusInternalServerError, errors.New("invalid name field for value"))
					return
				}



@@ 347,7 347,7 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {

			parts := strings.Split(key, "-")
			if len(parts) < 2 {
				c.Error(w, r, http.StatusInternalServerError, "invalid name field for value")
				c.Error(w, r, http.StatusInternalServerError, errors.New("invalid name field for value"))
				return
			}



@@ 363,7 363,7 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {

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



@@ 380,12 380,12 @@ func (c *Content) delete(w http.ResponseWriter, r *http.Request) {

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

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



@@ 407,19 407,19 @@ func (c *Content) search(w http.ResponseWriter, r *http.Request) {

	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, err)
		c.Error(w, r, http.StatusBadRequest, err)
		return
	}

	space, err := c.db.SpaceGet(user, spaceID)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, ErrNoSpace)
		c.Error(w, r, http.StatusBadRequest, errors.Wrap(err, ErrNoSpace.Error()))
		return
	}

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


M internal/c/contenttype/contenttype.go => internal/c/contenttype/contenttype.go +25 -25
@@ 75,7 75,7 @@ func (ct *ContentType) create(w http.ResponseWriter, r *http.Request) {

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



@@ 94,7 94,7 @@ func (ct *ContentType) create(w http.ResponseWriter, r *http.Request) {
		valType := r.FormValue(keyType)

		if valName == "" || valType == "" {
			ct.Error2(w, r, http.StatusBadRequest, ErrBadForm)
			ct.Error(w, r, http.StatusBadRequest, ErrBadForm)
			return
		}



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

	if len(params) < 1 {
		ct.Error2(w, r, http.StatusBadRequest, ErrNoFields)
		ct.Error(w, r, http.StatusBadRequest, ErrNoFields)
		return
	}



@@ 117,13 117,13 @@ func (ct *ContentType) create(w http.ResponseWriter, r *http.Request) {
		}
	}
	if !hasName {
		ct.Error2(w, r, http.StatusBadRequest, ErrNoNameField)
		ct.Error(w, r, http.StatusBadRequest, ErrNoNameField)
		return
	}

	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))
		ct.Error(w, r, http.StatusInternalServerError, fmt.Errorf("%s: %w", ErrFailedCreate.Error(), err))
		return
	}



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

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

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



@@ 164,7 164,7 @@ func (ct *ContentType) update(w http.ResponseWriter, r *http.Request) {
		valType := r.FormValue(keyType)

		if valName == "" || valType == "" {
			ct.Error2(w, r, http.StatusBadRequest, ErrBadForm)
			ct.Error(w, r, http.StatusBadRequest, ErrBadForm)
			return
		}



@@ 190,7 190,7 @@ func (ct *ContentType) update(w http.ResponseWriter, r *http.Request) {
		}

		if valName == "" || valType == "" || valID == "" {
			ct.Error2(w, r, http.StatusBadRequest, ErrBadForm)
			ct.Error(w, r, http.StatusBadRequest, ErrBadForm)
			return
		}



@@ 202,7 202,7 @@ func (ct *ContentType) update(w http.ResponseWriter, r *http.Request) {
	}

	if len(updateParams) < 1 {
		ct.Error2(w, r, http.StatusBadRequest, ErrNoFields)
		ct.Error(w, r, http.StatusBadRequest, ErrNoFields)
		return
	}



@@ 219,19 219,19 @@ func (ct *ContentType) update(w http.ResponseWriter, r *http.Request) {
		}
	}
	if !hasName {
		ct.Error2(w, r, http.StatusBadRequest, ErrNoNameField)
		ct.Error(w, r, http.StatusBadRequest, ErrNoNameField)
		return
	}

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

	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))
		ct.Error(w, r, http.StatusInternalServerError, fmt.Errorf("%s: %w", ErrFailedUpdate.Error(), err))
		return
	}



@@ 251,19 251,19 @@ func (c *ContentType) serve(w http.ResponseWriter, r *http.Request) {

	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, err)
		c.Error(w, r, http.StatusBadRequest, err)
		return
	}

	space, err := c.db.SpaceGet(user, spaceID)
	if err != nil {
		c.Error2(w, r, http.StatusInternalServerError, ErrNoSpace)
		c.Error(w, r, http.StatusInternalServerError, ErrNoSpace)
		return
	}

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



@@ 277,7 277,7 @@ func (c *ContentType) serve(w http.ResponseWriter, r *http.Request) {
	case "":
		o = db.OrderAsc
	default:
		c.Error2(w, r, http.StatusBadRequest, ErrBadOrder)
		c.Error(w, r, http.StatusBadRequest, ErrBadOrder)
		return
	}



@@ 290,7 290,7 @@ func (c *ContentType) serve(w http.ResponseWriter, r *http.Request) {
	before, _ := strconv.Atoi(r.URL.Query().Get("before"))
	list, err := c.db.ContentPerContentType(user, space, ct, before, o, f)
	if err != nil {
		c.Error2(w, r, http.StatusInternalServerError, ErrNoC)
		c.Error(w, r, http.StatusInternalServerError, ErrNoC)
		return
	}



@@ 308,24 308,24 @@ func (c *ContentType) delete(w http.ResponseWriter, r *http.Request) {

	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, err)
		c.Error(w, r, http.StatusBadRequest, err)
		return
	}

	space, err := c.db.SpaceGet(user, spaceID)
	if err != nil {
		c.Error2(w, r, http.StatusInternalServerError, ErrNoSpace)
		c.Error(w, r, http.StatusInternalServerError, ErrNoSpace)
		return
	}

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

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



@@ 339,20 339,20 @@ func (c *ContentType) search(w http.ResponseWriter, r *http.Request) {

	user, err := c.GetCookieUser(w, r)
	if err != nil {
		c.Error2(w, r, http.StatusBadRequest, err)
		c.Error(w, r, http.StatusBadRequest, err)
		return
	}

	space, err := c.db.SpaceGet(user, spaceID)
	if err != nil {
		c.Error2(w, r, http.StatusInternalServerError, ErrNoSpace)
		c.Error(w, r, http.StatusInternalServerError, ErrNoSpace)
		return
	}

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


M internal/c/file/file.go => internal/c/file/file.go +2 -2
@@ 36,14 36,14 @@ func New(c *c.Controller, log *log.Logger, db DBer, e3 E3er, baseURL string) *Fi
func (f *File) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ok, err := f.db.FileExists(f.baseURL + r.URL.Path)
	if !ok || err != nil {
		f.Error(w, r, http.StatusNotFound, "file does not exist")
		f.ErrorString(w, r, http.StatusNotFound, "file does not exist")
		return
	}

	full := strings.TrimRight(f.e3.URL(), "api") + strings.TrimLeft(r.URL.Path, "/file") // TODO: Cleanup, this is hacky.
	bytes, err := f.e3.Proxy(r.Context(), full)
	if err != nil {
		f.Error(w, r, http.StatusInternalServerError, "failed to serve file")
		f.ErrorString(w, r, http.StatusInternalServerError, "failed to serve file")
		return
	}


M internal/c/hook/hook.go => internal/c/hook/hook.go +8 -8
@@ 49,7 49,7 @@ func New(c *c.Controller, log *log.Logger, db dber) *Content {
func (h *Content) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	user, err := h.GetCookieUser(w, r)
	if err != nil {
		h.Error2(w, r, http.StatusBadRequest, err)
		h.Error(w, r, http.StatusBadRequest, err)
		return
	}



@@ 58,13 58,13 @@ func (h *Content) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	case "POST":
		space, err := h.db.SpaceGet(user, r.FormValue("space"))
		if err != nil {
			h.Error2(w, r, http.StatusBadRequest, ErrNoSpace)
			h.Error(w, r, http.StatusBadRequest, ErrNoSpace)
			return
		}

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



@@ 74,18 74,18 @@ func (h *Content) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	case "DELETE":
		space, err := h.db.SpaceGet(user, r.FormValue("space"))
		if err != nil {
			h.Error2(w, r, http.StatusBadRequest, ErrNoSpace)
			h.Error(w, r, http.StatusBadRequest, ErrNoSpace)
			return
		}

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

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



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

		space, err := h.db.SpaceGet(user, spaceID)
		if err != nil {
			h.Error2(w, r, http.StatusBadRequest, ErrNoSpace)
			h.Error(w, r, http.StatusBadRequest, ErrNoSpace)
			return
		}

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


M internal/c/invite/invite.go => internal/c/invite/invite.go +10 -10
@@ 47,7 47,7 @@ func (i Invite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
			// This is the logged out user.
			tok := strings.Trim(r.URL.Path, "/")
			if tok == "" {
				i.Error2(w, r, http.StatusBadRequest, ErrNoInvite)
				i.Error(w, r, http.StatusBadRequest, ErrNoInvite)
				return
			}
			i.HTML(w, r, inviteHTML, map[string]interface{}{"Invite": tok})


@@ 56,7 56,7 @@ func (i Invite) ServeHTTP(w http.ResponseWriter, r *http.Request) {

		invites, err := i.db.InviteList(user, user.Org())
		if err != nil {
			i.Error2(w, r, http.StatusInternalServerError, err)
			i.Error(w, r, http.StatusInternalServerError, err)
			return
		}



@@ 70,23 70,23 @@ func (i Invite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	case "POST":
		user, err := i.GetCookieUser(w, r)
		if err != nil {
			i.Error2(w, r, http.StatusBadRequest, c.ErrNoLogin)
			i.Error(w, r, http.StatusBadRequest, c.ErrNoLogin)
			return
		}

		role, ok := role.ByName(r.FormValue("role"))
		if !ok {
			i.Error2(w, r, http.StatusBadRequest, errors.New("invalid role suppplied for invite"))
			i.Error(w, r, http.StatusBadRequest, errors.New("invalid role suppplied for invite"))
			return
		}

		_, err = i.db.InviteNew(user, user.Org(), role)
		if errors.Is(err, invite.ErrExpired) || errors.Is(err, invite.ErrUsed) {
			i.Error2(w, r, http.StatusBadRequest, err)
			i.Error(w, r, http.StatusBadRequest, err)
			return
		}
		if err != nil {
			i.Error2(w, r, http.StatusInternalServerError, fmt.Errorf("failed to create invite: %w", err))
			i.Error(w, r, http.StatusInternalServerError, fmt.Errorf("failed to create invite: %w", err))
			return
		}



@@ 97,24 97,24 @@ func (i Invite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
		// Can't be logged in.
		_, err := i.GetCookieUser(w, r)
		if err == nil {
			i.Error2(w, r, http.StatusBadRequest, ErrLoggedIn)
			i.Error(w, r, http.StatusBadRequest, ErrLoggedIn)
			return
		}

		inv, err := i.db.InviteGetByToken(r.FormValue("invite"))
		if err != nil {
			i.Error2(w, r, http.StatusBadRequest, err)
			i.Error(w, r, http.StatusBadRequest, err)
			return
		}

		// Accept invite and create new user.
		user, _, err := i.db.InviteAccept(inv, r.FormValue("username"), r.FormValue("password"), r.FormValue("verify"))
		if errors.Is(err, invite.ErrExpired) || errors.Is(err, invite.ErrUsed) {
			i.Error2(w, r, http.StatusBadRequest, err)
			i.Error(w, r, http.StatusBadRequest, err)
			return
		}
		if err != nil {
			i.Error2(w, r, http.StatusInternalServerError, fmt.Errorf("failed to create invite: %w", err))
			i.Error(w, r, http.StatusInternalServerError, fmt.Errorf("failed to create invite: %w", err))
			return
		}


M internal/c/space/space.go => internal/c/space/space.go +15 -15
@@ 58,27 58,27 @@ func (s *Space) serve(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error2(w, r, http.StatusBadRequest, err)
		s.Error(w, r, http.StatusBadRequest, err)
		return
	}

	space, err := s.db.SpaceGet(user, spaceID)
	if err != nil {
		s.Error2(w, r, http.StatusNotFound, ErrNoSpace)
		s.Error(w, r, http.StatusNotFound, ErrNoSpace)
		return
	}

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

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



@@ 96,13 96,13 @@ func (s *Space) create(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(err, "can't create space"))
		s.Error(w, r, http.StatusBadRequest, errors.Wrap(err, "can't create space"))
		return
	}

	space, err := s.db.SpaceNew(user, name, desc)
	if err != nil {
		s.Error2(w, r, http.StatusBadRequest, err)
		s.Error(w, r, http.StatusBadRequest, err)
		return
	}



@@ 117,19 117,19 @@ func (s *Space) copy(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(err, "can't copy space"))
		s.Error(w, r, http.StatusBadRequest, errors.Wrap(err, "can't copy space"))
		return
	}

	spacePrev, err := s.db.SpaceGet(user, spaceID)
	if err != nil {
		s.Error2(w, r, http.StatusNotFound, ErrNoSpace)
		s.Error(w, r, http.StatusNotFound, ErrNoSpace)
		return
	}

	spaceNext, err := s.db.SpaceCopy(user, spacePrev, name, desc)
	if err != nil {
		s.Error(w, r, http.StatusBadRequest, err.Error())
		s.Error(w, r, http.StatusBadRequest, err)
		return
	}



@@ 144,19 144,19 @@ func (s *Space) update(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(err, "can't update space"))
		s.Error(w, r, http.StatusBadRequest, errors.Wrap(err, "can't update space"))
		return
	}

	prev, err := s.db.SpaceGet(user, spaceID)
	if err != nil {
		s.Error2(w, r, http.StatusNotFound, ErrNoSpace)
		s.Error(w, r, http.StatusNotFound, ErrNoSpace)
		return
	}

	next, err := s.db.SpaceUpdate(user, prev, name, desc)
	if err != nil {
		s.Error(w, r, http.StatusInternalServerError, "failed to update space")
		s.ErrorString(w, r, http.StatusInternalServerError, "failed to update space")
		return
	}



@@ 169,18 169,18 @@ func (s *Space) delete(w http.ResponseWriter, r *http.Request) {

	user, err := s.GetCookieUser(w, r)
	if err != nil {
		s.Error2(w, r, http.StatusBadRequest, errors.Wrap(err, "can't delete space"))
		s.Error(w, r, http.StatusBadRequest, errors.Wrap(err, "can't delete space"))
		return
	}

	space, err := s.db.SpaceGet(user, spaceID)
	if err != nil {
		s.Error2(w, r, http.StatusNotFound, ErrNoSpace)
		s.Error(w, r, http.StatusNotFound, ErrNoSpace)
		return
	}

	if err := s.db.SpaceDelete(user, space); err != nil {
		s.Error(w, r, http.StatusInternalServerError, "failed to delete space")
		s.ErrorString(w, r, http.StatusInternalServerError, "failed to delete space")
		return
	}


M internal/c/stripe/stripe.go => internal/c/stripe/stripe.go +1 -1
@@ 29,7 29,7 @@ func (s StripeEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	case "/success":
		err := s.stripe.CompleteCheckout(r.FormValue("session_id"))
		if err != nil {
			s.Error2(w, r, http.StatusInternalServerError, err)
			s.Error(w, r, http.StatusInternalServerError, err)
			return
		}
		s.Redirect(w, r, "/")

M internal/c/user/user.go => internal/c/user/user.go +15 -15
@@ 63,7 63,7 @@ func (l *User) login(w http.ResponseWriter, r *http.Request) {

	user, err := l.db.UserGet(username, password)
	if err != nil {
		l.Error2(w, r, http.StatusBadRequest, err)
		l.Error(w, r, http.StatusBadRequest, err)
		return
	}



@@ 73,7 73,7 @@ func (l *User) login(w http.ResponseWriter, r *http.Request) {

func (l *User) signup(w http.ResponseWriter, r *http.Request) {
	if !l.signupEnabled {
		l.Error2(w, r, http.StatusForbidden, ErrNoSignup)
		l.Error(w, r, http.StatusForbidden, ErrNoSignup)
		return
	}



@@ 82,13 82,13 @@ func (l *User) signup(w http.ResponseWriter, r *http.Request) {
	verify := r.FormValue("verify")
	t, ok := tier.ByName(r.FormValue("tier"))
	if !ok {
		l.Error2(w, r, http.StatusBadRequest, ErrNoTier)
		l.Error(w, r, http.StatusBadRequest, ErrNoTier)
		return
	}

	user, err := l.db.UserNew(username, password, verify)
	if err != nil {
		l.Error2(w, r, http.StatusBadRequest, err)
		l.Error(w, r, http.StatusBadRequest, err)
		return
	}
	l.SetCookieUser(w, r, user)


@@ 100,7 100,7 @@ func (l *User) signup(w http.ResponseWriter, r *http.Request) {

	stripeCheckoutSessionID, stripePK, err := l.stripe.StartCheckout(user, t)
	if err != nil {
		l.Error2(w, r, http.StatusInternalServerError, err)
		l.Error(w, r, http.StatusInternalServerError, err)
		return
	}



@@ 127,7 127,7 @@ func (l *User) home(w http.ResponseWriter, r *http.Request) {

	spaces, err := l.db.SpacesPerUser(user, before)
	if err != nil {
		l.Error(w, r, http.StatusInternalServerError, "failed to find spaces for user")
		l.Error(w, r, http.StatusInternalServerError, errors.New("failed to find spaces for user"))
		return
	}



@@ 141,12 141,12 @@ func (l *User) home(w http.ResponseWriter, r *http.Request) {
func (l *User) updateEmail(w http.ResponseWriter, r *http.Request) {
	u, err := l.GetCookieUser(w, r)
	if err != nil {
		l.Error2(w, r, http.StatusInternalServerError, c.ErrNoLogin)
		l.Error(w, r, http.StatusInternalServerError, c.ErrNoLogin)
		return
	}

	if _, err := l.db.UserSetEmail(u, r.FormValue("email")); err != nil {
		l.Error2(w, r, http.StatusInternalServerError, err)
		l.Error(w, r, http.StatusInternalServerError, err)
		return
	}



@@ 156,12 156,12 @@ func (l *User) updateEmail(w http.ResponseWriter, r *http.Request) {
func (l *User) updatePassword(w http.ResponseWriter, r *http.Request) {
	u, err := l.GetCookieUser(w, r)
	if err != nil {
		l.Error2(w, r, http.StatusInternalServerError, c.ErrNoLogin)
		l.Error(w, r, http.StatusInternalServerError, c.ErrNoLogin)
		return
	}

	if _, err := l.db.UserSetPassword(u, r.FormValue("current"), r.FormValue("password"), r.FormValue("verify")); err != nil {
		l.Error2(w, r, http.StatusInternalServerError, err)
		l.Error(w, r, http.StatusInternalServerError, err)
		return
	}



@@ 171,13 171,13 @@ func (l *User) updatePassword(w http.ResponseWriter, r *http.Request) {
func (l *User) updateBilling(w http.ResponseWriter, r *http.Request) {
	u, err := l.GetCookieUser(w, r)
	if err != nil {
		l.Error2(w, r, http.StatusInternalServerError, c.ErrNoLogin)
		l.Error(w, r, http.StatusInternalServerError, c.ErrNoLogin)
		return
	}

	t, ok := tier.ByName(r.FormValue("tier"))
	if !ok || !(t.Is(tier.Business) || t.Is(tier.Enterprise)) {
		l.Error2(w, r, http.StatusBadRequest, ErrNoTier)
		l.Error(w, r, http.StatusBadRequest, ErrNoTier)
		return
	}



@@ 188,7 188,7 @@ func (l *User) updateBilling(w http.ResponseWriter, r *http.Request) {

	stripeCheckoutSessionID, stripePK, err := l.stripe.StartCheckout(u, t)
	if err != nil {
		l.Error2(w, r, http.StatusInternalServerError, err)
		l.Error(w, r, http.StatusInternalServerError, err)
		return
	}



@@ 203,12 203,12 @@ func (l *User) updateBilling(w http.ResponseWriter, r *http.Request) {
func (l *User) cancelBilling(w http.ResponseWriter, r *http.Request) {
	u, err := l.GetCookieUser(w, r)
	if err != nil {
		l.Error2(w, r, http.StatusInternalServerError, c.ErrNoLogin)
		l.Error(w, r, http.StatusInternalServerError, c.ErrNoLogin)
		return
	}

	if err := l.stripe.CancelSubscription(u); err != nil {
		l.Error2(w, r, http.StatusInternalServerError, err)
		l.Error(w, r, http.StatusInternalServerError, err)
		return
	}