M TODO => TODO +2 -0
@@ 12,3 12,5 @@ Object storage implementation BYOB
Payment integration
When editing existing references don't blow away prev inputs
Invite a user (the user will have access to all the same spaces -- to your "org" basically)
+Migrations for user/org/space
+Save nav button broke on content page.
M cms.go => cms.go +2 -1
@@ 108,7 108,7 @@ func init() {
fs := e3.New(e3user, e3pass, e3url)
c := c.New(log.New(w, "[cms:content] ", 0), cacher, analyticsEnabled)
- libs := libstripe.New(stripeSuccessURL, stripeErrorURL, stripePK, stripeSK)
+ libs := libstripe.New(stripeSuccessURL, stripeErrorURL, stripePK, stripeSK, cacher)
app = &App{
applogger,
@@ 165,6 165,7 @@ func init() {
c,
log.New(w, "[cms:stripe] ", 0),
cacher,
+ libs,
stripeWebhookSecret,
)),
},
M internal/c/c_test.go => internal/c/c_test.go +4 -3
@@ 199,9 199,10 @@ func (s server4) ServeHTTP(w http.ResponseWriter, r *http.Request) {
type FakeUser struct{ u, p string }
-func (u FakeUser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
-func (u FakeUser) Name() string { return u.u }
-func (u FakeUser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u FakeUser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
+func (u FakeUser) Name() string { return u.u }
+func (u FakeUser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u FakeUser) OrgID() (r string) { return }
func TestUserFromBasicAuth(t *testing.T) {
t.Parallel()
M internal/c/content/content.go => internal/c/content/content.go +2 -2
@@ 19,11 19,11 @@ import (
"git.sr.ht/~evanj/cms/internal/m/valuetype"
"git.sr.ht/~evanj/cms/internal/s/db"
webhook "git.sr.ht/~evanj/cms/internal/s/hook"
- "git.sr.ht/~evanj/cms/internal/s/tmpl"
+ "git.sr.ht/~evanj/cms/internal/v"
)
var (
- contentHTML = tmpl.MustParse("html/content.html")
+ contentHTML = v.MustParse("html/content.html")
ErrNoLogin = c.ErrNoLogin
ErrNoSpace = errors.New("failed to find required space")
M internal/c/content/content_test.go => internal/c/content/content_test.go +4 -3
@@ 113,9 113,10 @@ func (pm umatcher) String() string {
type fakeuser struct{ u, p string }
-func (u fakeuser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
-func (u fakeuser) Name() string { return u.u }
-func (u fakeuser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u fakeuser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
+func (u fakeuser) Name() string { return u.u }
+func (u fakeuser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u fakeuser) OrgID() (r string) { return }
type fakespace struct{ n, d string }
M internal/c/contenttype/contenttype.go => internal/c/contenttype/contenttype.go +2 -2
@@ 14,11 14,11 @@ import (
"git.sr.ht/~evanj/cms/internal/m/space"
"git.sr.ht/~evanj/cms/internal/m/user"
"git.sr.ht/~evanj/cms/internal/s/db"
- "git.sr.ht/~evanj/cms/internal/s/tmpl"
+ "git.sr.ht/~evanj/cms/internal/v"
)
var (
- contenttypeHTML = tmpl.MustParse("html/contenttype.html")
+ contenttypeHTML = v.MustParse("html/contenttype.html")
ErrNoLogin = c.ErrNoLogin // TODO: Refactor.
ErrNoSpace = errors.New("failed to find required space")
M internal/c/contenttype/contenttype_test.go => internal/c/contenttype/contenttype_test.go +4 -3
@@ 44,9 44,10 @@ func TestNoUser(t *testing.T) {
type FakeUser struct{ u, p string }
-func (u FakeUser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
-func (u FakeUser) Name() string { return u.u }
-func (u FakeUser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u FakeUser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
+func (u FakeUser) Name() string { return u.u }
+func (u FakeUser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u FakeUser) OrgID() (r string) { return }
type FakeSpace struct{ n, d string }
M internal/c/doc/doc.go => internal/c/doc/doc.go +8 -8
@@ 7,17 7,17 @@ import (
"git.sr.ht/~evanj/cms/internal/c"
"git.sr.ht/~evanj/cms/internal/m/user"
- "git.sr.ht/~evanj/cms/internal/s/tmpl"
+ "git.sr.ht/~evanj/cms/internal/v"
)
var pages = map[string]*template.Template{
- "/page/doc": tmpl.MustParse("html/doc.html"),
- "/page/faq": tmpl.MustParse("html/faq.html"),
- "/page/terms": tmpl.MustParse("html/terms.html"),
- "/page/privacy": tmpl.MustParse("html/privacy.html"),
- "/page/contact": tmpl.MustParse("html/contact.html"),
- "/page/billing": tmpl.MustParse("html/billing.html"),
- "/page/stripe": tmpl.MustParse("html/stripe.html"),
+ "/page/doc": v.MustParse("html/doc.html"),
+ "/page/faq": v.MustParse("html/faq.html"),
+ "/page/terms": v.MustParse("html/terms.html"),
+ "/page/privacy": v.MustParse("html/privacy.html"),
+ "/page/contact": v.MustParse("html/contact.html"),
+ "/page/billing": v.MustParse("html/billing.html"),
+ "/page/stripe": v.MustParse("html/stripe.html"),
}
type Doc struct {
M internal/c/doc/doc_test.go => internal/c/doc/doc_test.go +18 -3
@@ 30,9 30,10 @@ func TestWithoutUser(t *testing.T) {
type FakeUser struct{}
-func (u FakeUser) ID() string { return "98" }
-func (u FakeUser) Name() string { return "Spike" }
-func (u FakeUser) Token() string { return uuid.New().String() }
+func (u FakeUser) ID() string { return "98" }
+func (u FakeUser) Name() string { return "Spike" }
+func (u FakeUser) Token() string { return uuid.New().String() }
+func (u FakeUser) OrgID() (r string) { return }
func TestWithUser(t *testing.T) {
t.Parallel()
@@ 76,3 77,17 @@ func TestWithInvalidUserToken(t *testing.T) {
res, _ := http.DefaultClient.Do(req)
assert.Equal(t, res.StatusCode, http.StatusOK)
}
+
+func TestNotFound(t *testing.T) {
+ t.Parallel()
+
+ var (
+ ctrl = gomock.NewController(t)
+ db = doc.NewMockdber(ctrl)
+ s = doc.New(c.New(nil, db, true), nil, db)
+ ts = httptest.NewServer(s)
+ )
+
+ res, _ := http.Get(ts.URL + "/page/aye,aye,captain")
+ assert.Equal(t, res.StatusCode, http.StatusNotFound)
+}
M internal/c/hook/hook.go => internal/c/hook/hook.go +2 -2
@@ 11,11 11,11 @@ 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"
- "git.sr.ht/~evanj/cms/internal/s/tmpl"
+ "git.sr.ht/~evanj/cms/internal/v"
)
var (
- hookHTML = tmpl.MustParse("html/hook.html")
+ hookHTML = v.MustParse("html/hook.html")
ErrNoSpace = errors.New("failed to find required space")
ErrNoHook = errors.New("failed to find desired webhook")
M internal/c/hook/hook_test.go => internal/c/hook/hook_test.go +4 -3
@@ 41,9 41,10 @@ func TestNoUser(t *testing.T) {
type FakeUser struct{ u, p string }
-func (u FakeUser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
-func (u FakeUser) Name() string { return u.u }
-func (u FakeUser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u FakeUser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
+func (u FakeUser) Name() string { return u.u }
+func (u FakeUser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u FakeUser) OrgID() (r string) { return }
type FakeSpace struct{ n, d string }
M internal/c/redirect/redirect.go => internal/c/redirect/redirect.go +2 -2
@@ 6,10 6,10 @@ import (
"git.sr.ht/~evanj/cms/internal/c"
"git.sr.ht/~evanj/cms/internal/m/user"
- "git.sr.ht/~evanj/cms/internal/s/tmpl"
+ "git.sr.ht/~evanj/cms/internal/v"
)
-var redirectHTML = tmpl.MustParse("html/redirect.html")
+var redirectHTML = v.MustParse("html/redirect.html")
type Redirect struct {
*c.Controller
M internal/c/space/space.go => internal/c/space/space.go +2 -2
@@ 14,11 14,11 @@ 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"
- "git.sr.ht/~evanj/cms/internal/s/tmpl"
+ "git.sr.ht/~evanj/cms/internal/v"
)
var (
- spaceHTML = tmpl.MustParse("html/space.html")
+ spaceHTML = v.MustParse("html/space.html")
ErrNoSpace = errors.New("failed to find space")
)
M internal/c/space/space_test.go => internal/c/space/space_test.go +1 -0
@@ 36,6 36,7 @@ func fakeuser(u, p string) FakeUser { return FakeUser{u, p} }
func (u FakeUser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
func (u FakeUser) Name() string { return u.u }
func (u FakeUser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u FakeUser) OrgID() string { return fmt.Sprintf("org-id-%s-%s", u.u, u.p) }
type FakeSpace struct{ n, d string }
M internal/c/stripe/stripe.go => internal/c/stripe/stripe.go +10 -63
@@ 1,23 1,18 @@
package stripe
import (
- "encoding/json"
- "fmt"
- "io/ioutil"
"log"
"net/http"
"git.sr.ht/~evanj/cms/internal/c"
"git.sr.ht/~evanj/cms/internal/m/user"
- "git.sr.ht/~evanj/cms/internal/s/stripe"
- libstripe "github.com/stripe/stripe-go/v71"
- webhook "github.com/stripe/stripe-go/v71/webhook"
)
type StripeEndpoint struct {
*c.Controller
log *log.Logger
db DBer
+ stripe Striper
stripeWebhookSecret string
}
@@ 25,75 20,27 @@ type DBer interface {
UserGetFromToken(token string) (user.User, error)
}
-func New(c *c.Controller, l *log.Logger, db DBer, stripeWebhookSecret string) *StripeEndpoint {
- return &StripeEndpoint{c, l, db, stripeWebhookSecret}
+type Striper interface {
+ CompleteCheckout(sessionID string) error
}
-func (s *StripeEndpoint) webhook(w http.ResponseWriter, r *http.Request) {
- // webhook from Stripe.
- bytes, err := ioutil.ReadAll(r.Body)
- if err != nil {
- s.ErrorString(w, r, http.StatusBadRequest, "failed to ready body")
- return
- }
-
- event, err := webhook.ConstructEvent(bytes, r.Header.Get("Stripe-Signature"), s.stripeWebhookSecret)
- if err != nil {
- s.log.Println("you do not have access")
- s.ErrorString(w, r, http.StatusForbidden, "you do not have access")
- return
- }
-
- switch event.Type {
- case "payment_intent.succeeded":
- var pi libstripe.PaymentIntent
- err := json.Unmarshal(event.Data.Raw, &pi)
- if err != nil {
- s.Error2(w, r, http.StatusBadRequest, fmt.Errorf("error parsing webhook JSON: %w\n", err))
- return
- }
- if pi.Customer == nil {
- s.ErrorString(w, r, http.StatusBadRequest, "no customer for payment")
- return
- }
-
- userToken, ok := pi.Customer.Metadata[stripe.KeyUserToken]
- if !ok {
- s.ErrorString(w, r, http.StatusBadRequest, "no user associated with customer")
- return
- }
-
- user, err := s.db.UserGetFromToken(userToken)
- if err != nil {
- s.ErrorString(w, r, http.StatusBadRequest, "no user for token")
- return
- }
-
- // TODO: Add payment to user/org.
- _ = user
- s.log.Println("success with getting user from PI")
-
- default:
- s.Error2(w, r, http.StatusBadRequest, fmt.Errorf("unexpected event type: %s\n", event.Type))
- return
- }
-
- s.log.Println("successfully completed stripe webhook")
- s.Redirect(w, r, "/")
+func New(c *c.Controller, l *log.Logger, db DBer, striper Striper, stripeWebhookSecret string) *StripeEndpoint {
+ return &StripeEndpoint{c, l, db, striper, stripeWebhookSecret}
}
func (s *StripeEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // Handle users requests.
switch r.URL.Path {
case "/success":
- s.log.Println("redirect user success")
+ err := s.stripe.CompleteCheckout(r.FormValue("session_id"))
+ if err != nil {
+ s.Error2(w, r, http.StatusInternalServerError, err)
+ }
s.Redirect(w, r, "/")
return
case "/error":
- s.log.Println("user error")
s.ErrorString(w, r, http.StatusInternalServerError, "failed to process request: you have not been charged")
return
}
- s.webhook(w, r)
+ http.NotFound(w, r)
}
R internal/c/user/mock/mock_user.go => internal/c/user/mock.go +43 -4
@@ 1,15 1,15 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: user.go
-// Package mock_user is a generated GoMock package.
-package mock_user
+// Package user is a generated GoMock package.
+package user
import (
- reflect "reflect"
-
space "git.sr.ht/~evanj/cms/internal/m/space"
+ tier "git.sr.ht/~evanj/cms/internal/m/tier"
user "git.sr.ht/~evanj/cms/internal/m/user"
gomock "github.com/golang/mock/gomock"
+ reflect "reflect"
)
// Mockdber is a mock of dber interface
@@ 94,3 94,42 @@ func (mr *MockdberMockRecorder) SpacesPerUser(user, before interface{}) *gomock.
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpacesPerUser", reflect.TypeOf((*Mockdber)(nil).SpacesPerUser), user, before)
}
+
+// MockStriper is a mock of Striper interface
+type MockStriper struct {
+ ctrl *gomock.Controller
+ recorder *MockStriperMockRecorder
+}
+
+// MockStriperMockRecorder is the mock recorder for MockStriper
+type MockStriperMockRecorder struct {
+ mock *MockStriper
+}
+
+// NewMockStriper creates a new mock instance
+func NewMockStriper(ctrl *gomock.Controller) *MockStriper {
+ mock := &MockStriper{ctrl: ctrl}
+ mock.recorder = &MockStriperMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockStriper) EXPECT() *MockStriperMockRecorder {
+ return m.recorder
+}
+
+// StartCheckout mocks base method
+func (m *MockStriper) StartCheckout(user user.User, t tier.Tier) (string, string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "StartCheckout", user, t)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(string)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// StartCheckout indicates an expected call of StartCheckout
+func (mr *MockStriperMockRecorder) StartCheckout(user, t interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartCheckout", reflect.TypeOf((*MockStriper)(nil).StartCheckout), user, t)
+}
M internal/c/user/user.go => internal/c/user/user.go +2 -4
@@ 12,11 12,11 @@ import (
"git.sr.ht/~evanj/cms/internal/m/space"
"git.sr.ht/~evanj/cms/internal/m/tier"
"git.sr.ht/~evanj/cms/internal/m/user"
- "git.sr.ht/~evanj/cms/internal/s/tmpl"
+ "git.sr.ht/~evanj/cms/internal/v"
)
var (
- indexHTML = tmpl.MustParse("html/index.html")
+ indexHTML = v.MustParse("html/index.html")
ErrNoUser = errors.New("incorrect user credentials")
ErrNoSignup = errors.New("signups are forbidden at this time")
ErrNoTier = errors.New("invalid tier")
@@ 117,10 117,8 @@ func (l *User) home(w http.ResponseWriter, r *http.Request) {
if err != nil {
l.HTML(w, r, indexHTML, map[string]interface{}{
"Tiers": tier.Tiers,
- "User": user,
})
return
-
}
// Don't care about the error value here. When error occurs before is zero
M internal/c/user/user_test.go => internal/c/user/user_test.go +112 -34
@@ 10,8 10,8 @@ import (
"git.sr.ht/~evanj/cms/internal/c"
"git.sr.ht/~evanj/cms/internal/c/user"
- mock_user "git.sr.ht/~evanj/cms/internal/c/user/mock"
spaceType "git.sr.ht/~evanj/cms/internal/m/space"
+ tier "git.sr.ht/~evanj/cms/internal/m/tier"
"github.com/bmizerany/assert"
"github.com/golang/mock/gomock"
"github.com/google/uuid"
@@ 19,9 19,10 @@ import (
type FakeUser struct{ u, p string }
-func (u FakeUser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
-func (u FakeUser) Name() string { return u.u }
-func (u FakeUser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u FakeUser) ID() string { return fmt.Sprintf("id-%s-%s", u.u, u.p) }
+func (u FakeUser) Name() string { return u.u }
+func (u FakeUser) Token() string { return fmt.Sprintf("token-%s-%s", u.u, u.p) }
+func (u FakeUser) OrgID() (r string) { return }
type FakeSpaceList struct{}
@@ 33,11 34,12 @@ func TestCreate(t *testing.T) {
t.Parallel()
var (
- ctrl = gomock.NewController(t)
- db = mock_user.NewMockdber(ctrl)
- l *log.Logger
- s = user.New(c.New(l, db, true), l, db, true)
- ts = httptest.NewServer(s)
+ ctrl = gomock.NewController(t)
+ db = user.NewMockdber(ctrl)
+ l *log.Logger
+ stripe = user.NewMockStriper(ctrl)
+ s = user.New(c.New(l, db, true), l, db, true, stripe)
+ ts = httptest.NewServer(s)
uname = uuid.New().String()
upass = uuid.New().String()
@@ 47,6 49,7 @@ func TestCreate(t *testing.T) {
"username": {uname},
"password": {upass},
"verify": {upass},
+ "tier": {tier.Free.Name},
}
)
@@ 60,11 63,12 @@ func TestLogin(t *testing.T) {
t.Parallel()
var (
- ctrl = gomock.NewController(t)
- db = mock_user.NewMockdber(ctrl)
- l *log.Logger
- s = user.New(c.New(l, db, true), l, db, true)
- ts = httptest.NewServer(s)
+ ctrl = gomock.NewController(t)
+ db = user.NewMockdber(ctrl)
+ l *log.Logger
+ stripe = user.NewMockStriper(ctrl)
+ s = user.New(c.New(l, db, true), l, db, true, stripe)
+ ts = httptest.NewServer(s)
uname = uuid.New().String()
upass = uuid.New().String()
@@ 86,11 90,12 @@ func TestLogout(t *testing.T) {
t.Parallel()
var (
- ctrl = gomock.NewController(t)
- db = mock_user.NewMockdber(ctrl)
- l *log.Logger
- s = user.New(c.New(l, db, true), l, db, true)
- ts = httptest.NewServer(s)
+ ctrl = gomock.NewController(t)
+ db = user.NewMockdber(ctrl)
+ l *log.Logger
+ stripe = user.NewMockStriper(ctrl)
+ s = user.New(c.New(l, db, true), l, db, true, stripe)
+ ts = httptest.NewServer(s)
)
res, _ := http.PostForm(ts.URL+"/user/logout", url.Values{})
@@ 101,11 106,12 @@ func TestHome(t *testing.T) {
t.Parallel()
var (
- ctrl = gomock.NewController(t)
- db = mock_user.NewMockdber(ctrl)
- l *log.Logger
- s = user.New(c.New(l, db, true), l, db, true)
- ts = httptest.NewServer(s)
+ ctrl = gomock.NewController(t)
+ db = user.NewMockdber(ctrl)
+ l *log.Logger
+ stripe = user.NewMockStriper(ctrl)
+ s = user.New(c.New(l, db, true), l, db, true, stripe)
+ ts = httptest.NewServer(s)
uname = uuid.New().String()
upass = uuid.New().String()
@@ 115,6 121,7 @@ func TestHome(t *testing.T) {
"username": {uname},
"password": {upass},
"verify": {upass},
+ "tier": {tier.Free.Name},
}
sl = FakeSpaceList{}
@@ 138,11 145,12 @@ func TestBadLogin(t *testing.T) {
t.Parallel()
var (
- ctrl = gomock.NewController(t)
- db = mock_user.NewMockdber(ctrl)
- l *log.Logger
- s = user.New(c.New(l, db, true), l, db, true)
- ts = httptest.NewServer(s)
+ ctrl = gomock.NewController(t)
+ db = user.NewMockdber(ctrl)
+ l *log.Logger
+ stripe = user.NewMockStriper(ctrl)
+ s = user.New(c.New(l, db, true), l, db, true, stripe)
+ ts = httptest.NewServer(s)
uname = uuid.New().String()
upass = uuid.New().String()
@@ 163,11 171,12 @@ func TestNoSignups(t *testing.T) {
t.Parallel()
var (
- ctrl = gomock.NewController(t)
- db = mock_user.NewMockdber(ctrl)
- l *log.Logger
- s = user.New(c.New(l, db, true), l, db, false)
- ts = httptest.NewServer(s)
+ ctrl = gomock.NewController(t)
+ db = user.NewMockdber(ctrl)
+ l *log.Logger
+ stripe = user.NewMockStriper(ctrl)
+ s = user.New(c.New(l, db, true), l, db, false, stripe)
+ ts = httptest.NewServer(s)
uname = uuid.New().String()
upass = uuid.New().String()
@@ 176,6 185,7 @@ func TestNoSignups(t *testing.T) {
"username": {uname},
"password": {upass},
"verify": {upass},
+ "tier": {tier.Free.Name},
}
)
@@ 184,3 194,71 @@ func TestNoSignups(t *testing.T) {
res, _ := http.PostForm(ts.URL+"/user/signup", form)
assert.Equal(t, res.StatusCode, http.StatusForbidden)
}
+
+func TestCreatePaid(t *testing.T) {
+ t.Parallel()
+
+ var (
+ ctrl = gomock.NewController(t)
+ db = user.NewMockdber(ctrl)
+ l *log.Logger
+ stripe = user.NewMockStriper(ctrl)
+ s = user.New(c.New(l, db, true), l, db, true, stripe)
+ ts = httptest.NewServer(s)
+
+ uname = uuid.New().String()
+ upass = uuid.New().String()
+ u = FakeUser{uname, upass}
+
+ form = url.Values{
+ "username": {uname},
+ "password": {upass},
+ "verify": {upass},
+ "tier": {tier.Business.Name},
+ }
+ )
+
+ db.EXPECT().UserNew(uname, upass, upass).Return(u, nil).AnyTimes()
+ stripe.EXPECT().StartCheckout(u, tier.Business).Return("SomeSessionID", "SomePublicKey", nil).AnyTimes()
+
+ res, _ := http.PostForm(ts.URL+"/user/signup", form)
+ assert.Equal(t, res.StatusCode, http.StatusOK)
+}
+
+func TestHomeNoUser(t *testing.T) {
+ t.Parallel()
+
+ var (
+ ctrl = gomock.NewController(t)
+ db = user.NewMockdber(ctrl)
+ l *log.Logger
+ stripe = user.NewMockStriper(ctrl)
+ s = user.New(c.New(l, db, true), l, db, true, stripe)
+ ts = httptest.NewServer(s)
+
+ uname = uuid.New().String()
+ upass = uuid.New().String()
+ u = FakeUser{uname, upass}
+
+ form = url.Values{
+ "username": {uname},
+ "password": {upass},
+ "verify": {upass},
+ "tier": {tier.Free.Name},
+ }
+
+ sl = FakeSpaceList{}
+ )
+
+ db.EXPECT().UserNew(uname, upass, upass).Return(u, nil).AnyTimes()
+ db.EXPECT().UserGet(uname, upass).Return(u, nil).AnyTimes()
+ db.EXPECT().SpacesPerUser(u, 0).Return(sl, nil).AnyTimes()
+
+ res, _ := http.PostForm(ts.URL+"/user/signup", form)
+ assert.Equal(t, res.StatusCode, http.StatusOK)
+
+ req, _ := http.NewRequest("POST", ts.URL, nil)
+ req.Header.Set("Accept", "text/html")
+ res, _ = http.DefaultClient.Do(req)
+ assert.Equal(t, res.StatusCode, http.StatusOK)
+}
M internal/m/user/user.go => internal/m/user/user.go +1 -0
@@ 4,4 4,5 @@ type User interface {
ID() string
Name() string
Token() string
+ OrgID() string
}
M internal/s/db/db.go => internal/s/db/db.go +84 -240
@@ 3,6 3,7 @@ package db
import (
"database/sql"
"log"
+ "sort"
"strconv"
"strings"
@@ 11,6 12,26 @@ import (
_ "github.com/go-sql-driver/mysql"
)
+//go:generate embed -pattern */* -id migrations
+
+func newMigrationSlice(m map[string]string) [][]string {
+ migrations := make([][]string, len(m))
+ i := 0
+
+ for key, val := range m {
+ migrations[i] = []string{key, val}
+ i++
+ }
+
+ sort.Slice(migrations, func(i, j int) bool {
+ iKey := migrations[i][0]
+ jKey := migrations[j][0]
+ return strings.Compare(iKey, jKey) < 1
+ })
+
+ return migrations
+}
+
const (
// Default pagination amount. For use in LIMIT/OFFSET.
perPage = 25
@@ 102,243 123,80 @@ func NewWithConn(log *log.Logger, sec securer, conn *sql.DB) (*DB, error) {
// migrate does our "migration" -migration in quotes as we just dummy
// attempt to create tables on every server startup and ignore "table already
// exists" errors.
-func (db *DB) migrate() []error {
- var errors []error
- var err error
-
- var _ interface{}
-
- // user
- _, err = db.Exec(`
- CREATE TABLE cms_user (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- NAME varchar(256) UNIQUE NOT NULL,
- HASH varchar(256) NOT NULL
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // space
- _, err = db.Exec(`
- CREATE TABLE cms_space (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- NAME varchar(256) NOT NULL,
- DESCRIPTION varchar(256) NOT NULL
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // user to space
- _, err = db.Exec(`
- CREATE TABLE cms_user_to_space (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- USER_ID INTEGER NOT NULL,
- SPACE_ID INTEGER NOT NULL,
- FOREIGN KEY(USER_ID) REFERENCES cms_user(ID),
- FOREIGN KEY(SPACE_ID) REFERENCES cms_space(ID) ON DELETE CASCADE
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // contenttype
- _, err = db.Exec(`
- CREATE TABLE cms_contenttype (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- NAME varchar(256) NOT NULL,
- SPACE_ID INTEGER NOT NULL,
- CONSTRAINT FG FOREIGN KEY(SPACE_ID) REFERENCES cms_space(ID) ON DELETE CASCADE,
- CONSTRAINT UNIQUEPERCONN UNIQUE(SPACE_ID, NAME)
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // valuetype
- // This will never be created by users.
- _, err = db.Exec(`
- CREATE TABLE cms_valuetype (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- VALUE varchar(256) UNIQUE NOT NULL
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // contenttype to valuetype
- // TODO: Make name + contenttype_id unique.
- _, err = db.Exec(`
- CREATE TABLE cms_contenttype_to_valuetype (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- NAME varchar(256) NOT NULL,
- CONTENTTYPE_ID INTEGER NOT NULL,
- VALUETYPE_ID INTEGER NOT NULL,
- FOREIGN KEY(CONTENTTYPE_ID) REFERENCES cms_contenttype(ID) ON DELETE CASCADE,
- FOREIGN KEY(VALUETYPE_ID) REFERENCES cms_valuetype(ID)
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // content
- _, err = db.Exec(`
- CREATE TABLE cms_content (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- CONTENTTYPE_ID INTEGER NOT NULL,
- FOREIGN KEY(CONTENTTYPE_ID) REFERENCES cms_contenttype(ID) ON DELETE CASCADE
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // content_to_value
- _, err = db.Exec(`
- CREATE TABLE cms_value (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- CONTENT_ID INTEGER NOT NULL,
- CONTENTTYPE_TO_VALUETYPE_ID INTEGER NOT NULL,
- VALUE_ID INTEGER NOT NULL, -- Should be a foreign key but impossible to make it for two+ tables.
- FOREIGN KEY(CONTENT_ID) REFERENCES cms_content(ID) ON DELETE CASCADE,
- FOREIGN KEY(CONTENTTYPE_TO_VALUETYPE_ID) REFERENCES cms_contenttype_to_valuetype(ID) ON DELETE CASCADE
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // value StringSmall, File
- _, err = db.Exec(`
- CREATE TABLE cms_value_string_small (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- VALUE VARCHAR(256) NOT NULL
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // value StringBig, InputHTML, InputMarkdown
- _, err = db.Exec(`
- CREATE TABLE cms_value_string_big (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- VALUE TEXT NOT NULL
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // value Date
- _, err = db.Exec(`
- CREATE TABLE cms_value_date (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- VALUE DATE NOT NULL
- );
- `)
-
- // TODO: Reconsider these ON DELETE CASCADES after this point.
-
- // value Reference
- _, err = db.Exec(`
- CREATE TABLE cms_value_reference (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- VALUE INTEGER NOT NULL,
- FOREIGN KEY(VALUE) REFERENCES cms_content(ID) ON DELETE CASCADE
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // value ReferenceList
- _, err = db.Exec(`
- CREATE TABLE cms_value_reference_list (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // augment to ReferenceList
- _, err = db.Exec(`
- CREATE TABLE cms_value_reference_list_values (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- VALUE_ID INTEGER NOT NULL,
- CONTENT_ID INTEGER NOT NULL,
- FOREIGN KEY(VALUE_ID) REFERENCES cms_value_reference_list(ID) ON DELETE CASCADE,
- FOREIGN KEY(CONTENT_ID) REFERENCES cms_content(ID) ON DELETE CASCADE
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
-
- // Webhook
- _, err = db.Exec(`
- CREATE TABLE cms_hooks (
- ID INTEGER PRIMARY KEY AUTO_INCREMENT,
- URL varchar(256) NOT NULL,
- SPACE_ID INTEGER NOT NULL,
- FOREIGN KEY(SPACE_ID) REFERENCES cms_space(ID) ON DELETE CASCADE,
- CONSTRAINT UNIQUEPERCONN UNIQUE(SPACE_ID, URL)
- );
- `)
- if err != nil {
- errors = append(errors, err)
- }
+func (db *DB) migrate() error {
+ var (
+ err error
+ _ interface{}
+ )
+
+ for _, migrationSet := range newMigrationSlice(migrations) {
+ key := migrationSet[0]
+ m := migrationSet[1]
+
+ var count int
+ if err := db.QueryRow("SELECT COUNT(*) FROM cms_migrate WHERE NAME=?", key).Scan(&count); err != nil {
+ // Catch first error of DB setup.
+ if !strings.Contains(err.Error(), "cms_migrate' doesn't exist") {
+ return err
+ }
+ }
- // Only valuetypes cms supports.
- _, err = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.StringSmall)
- if err != nil {
- errors = append(errors, err)
- }
+ if count > 0 {
+ continue
+ }
- _, err = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.StringBig)
- if err != nil {
- errors = append(errors, err)
- }
+ t, err := db.Begin()
+ if err != nil {
+ return err
+ }
+ defer t.Rollback()
+
+ for _, q := range strings.Split(m, ";") {
+ q = strings.TrimSpace(q)
+ if q == "" {
+ continue
+ }
+ if _, err := t.Exec(q); err != nil {
+ return err
+ }
+ }
- _, err = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.InputHTML)
- if err != nil {
- errors = append(errors, err)
- }
+ if err := t.Commit(); err != nil {
+ return err
+ }
- _, err = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.InputMarkdown)
- if err != nil {
- errors = append(errors, err)
+ if _, err := db.Exec("INSERT INTO cms_migrate (NAME) VALUES (?)", key); err != nil {
+ return err
+ }
}
- _, err = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.File)
- if err != nil {
- errors = append(errors, err)
+ vtypes := []valuetype.ValueTypeEnum{
+ valuetype.StringSmall,
+ valuetype.StringBig,
+ valuetype.InputHTML,
+ valuetype.InputMarkdown,
+ valuetype.File,
+ valuetype.Date,
+ valuetype.Reference,
+ valuetype.ReferenceList,
}
- _, err = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.Date)
- if err != nil {
- errors = append(errors, err)
- }
+ for _, vt := range vtypes {
+ var count int
+ if err := db.QueryRow("SELECT COUNT(*) FROM cms_valuetype WHERE VALUE=?", count).Scan(&count); err != nil {
+ return err
+ }
- _, err = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.Reference)
- if err != nil {
- errors = append(errors, err)
- }
+ if count > 0 {
+ continue
+ }
- _, err = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.ReferenceList)
- if err != nil {
- errors = append(errors, err)
+ if _, err = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, vt); err != nil {
+ return err
+ }
}
- return errors
+ return nil
}
// Ensure we have the tables we require. If we receive an error other
@@ 347,21 205,7 @@ func (db *DB) migrate() []error {
// Mainly, we are runnin CREATE TABLE and some INSERT INTO of predefined
// value types.
func (db *DB) EnsureSetup() error {
- for _, err := range db.migrate() {
- errmsg := err.Error()
-
- if err != nil && strings.Contains(errmsg, "Table ") && strings.Contains(errmsg, "already exists") {
- continue
- }
-
- if err != nil && strings.Contains(errmsg, "Duplicate entry ") && strings.Contains(errmsg, "VALUE") {
- continue
- }
-
- return err
- }
-
- return nil
+ return db.migrate()
}
// FileExists makes sure SOME space and content owns the file. I.E. deleted
A internal/s/db/migrations_embed.go => internal/s/db/migrations_embed.go +33 -0
@@ 0,0 1,33 @@
+// Code generated by "embed -pattern */* -id migrations"; DO NOT EDIT.
+
+package db
+
+import "encoding/base64"
+
+var migrations map[string]string
+
+func tostring(in string) string {
+ bytes, _ := base64.StdEncoding.DecodeString(in)
+ return string(bytes)
+}
+
+func init() {
+ migrations = make(map[string]string)
+
+ migrations["sql/00001.sql"] = tostring("Q1JFQVRFIFRBQkxFIGNtc19taWdyYXRlICgKCUlEIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT19JTkNSRU1FTlQsCglOQU1FIHZhcmNoYXIoMjU2KSBVTklRVUUgTk9UIE5VTEwsCiAgREFURSBUSU1FU1RBTVAgTk9UIE5VTEwgREVGQVVMVCBDVVJSRU5UX1RJTUVTVEFNUAopOwoKQ1JFQVRFIFRBQkxFIGNtc191c2VyICgKCUlEIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT19JTkNSRU1FTlQsCglOQU1FIHZhcmNoYXIoMjU2KSBVTklRVUUgTk9UIE5VTEwsCglIQVNIIHZhcmNoYXIoMjU2KSBOT1QgTlVMTAopOwoKQ1JFQVRFIFRBQkxFIGNtc19zcGFjZSAoCglJRCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9fSU5DUkVNRU5ULAoJTkFNRSB2YXJjaGFyKDI1NikgTk9UIE5VTEwsCglERVNDUklQVElPTiB2YXJjaGFyKDI1NikgTk9UIE5VTEwKKTsKCkNSRUFURSBUQUJMRSBjbXNfdXNlcl90b19zcGFjZSAoCglJRCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9fSU5DUkVNRU5ULAoJVVNFUl9JRCBJTlRFR0VSIE5PVCBOVUxMLAoJU1BBQ0VfSUQgSU5URUdFUiBOT1QgTlVMTCwKCUZPUkVJR04gS0VZKFVTRVJfSUQpIFJFRkVSRU5DRVMgY21zX3VzZXIoSUQpLAoJRk9SRUlHTiBLRVkoU1BBQ0VfSUQpIFJFRkVSRU5DRVMgY21zX3NwYWNlKElEKSBPTiBERUxFVEUgQ0FTQ0FERQopOwoKQ1JFQVRFIFRBQkxFIGNtc19jb250ZW50dHlwZSAoCglJRCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9fSU5DUkVNRU5ULAoJTkFNRSB2YXJjaGFyKDI1NikgTk9UIE5VTEwsCglTUEFDRV9JRCBJTlRFR0VSIE5PVCBOVUxMLAoJQ09OU1RSQUlOVCBGRyBGT1JFSUdOIEtFWShTUEFDRV9JRCkgUkVGRVJFTkNFUyBjbXNfc3BhY2UoSUQpIE9OIERFTEVURSBDQVNDQURFLAoJQ09OU1RSQUlOVCBVTklRVUVQRVJDT05OIFVOSVFVRShTUEFDRV9JRCwgTkFNRSkKKTsKCkNSRUFURSBUQUJMRSBjbXNfdmFsdWV0eXBlICgKCUlEIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT19JTkNSRU1FTlQsCglWQUxVRSB2YXJjaGFyKDI1NikgVU5JUVVFIE5PVCBOVUxMCik7CgpDUkVBVEUgVEFCTEUgY21zX2NvbnRlbnR0eXBlX3RvX3ZhbHVldHlwZSAoCglJRCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9fSU5DUkVNRU5ULAoJTkFNRSB2YXJjaGFyKDI1NikgTk9UIE5VTEwsCglDT05URU5UVFlQRV9JRCBJTlRFR0VSIE5PVCBOVUxMLAoJVkFMVUVUWVBFX0lEIElOVEVHRVIgTk9UIE5VTEwsCglGT1JFSUdOIEtFWShDT05URU5UVFlQRV9JRCkgUkVGRVJFTkNFUyBjbXNfY29udGVudHR5cGUoSUQpIE9OIERFTEVURSBDQVNDQURFLAoJRk9SRUlHTiBLRVkoVkFMVUVUWVBFX0lEKSBSRUZFUkVOQ0VTIGNtc192YWx1ZXR5cGUoSUQpCik7CgpDUkVBVEUgVEFCTEUgY21zX2NvbnRlbnQgKAoJSUQgSU5URUdFUiBQUklNQVJZIEtFWSBBVVRPX0lOQ1JFTUVOVCwKCUNPTlRFTlRUWVBFX0lEIElOVEVHRVIgTk9UIE5VTEwsCglGT1JFSUdOIEtFWShDT05URU5UVFlQRV9JRCkgUkVGRVJFTkNFUyBjbXNfY29udGVudHR5cGUoSUQpIE9OIERFTEVURSBDQVNDQURFCik7CgpDUkVBVEUgVEFCTEUgY21zX3ZhbHVlICgKCUlEIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT19JTkNSRU1FTlQsCglDT05URU5UX0lEIElOVEVHRVIgTk9UIE5VTEwsCglDT05URU5UVFlQRV9UT19WQUxVRVRZUEVfSUQgSU5URUdFUiBOT1QgTlVMTCwgCglWQUxVRV9JRCBJTlRFR0VSIE5PVCBOVUxMLCAtLSBTaG91bGQgYmUgYSBmb3JlaWduIGtleSBidXQgaW1wb3NzaWJsZSB0byBtYWtlIGl0IGZvciB0d28rIHRhYmxlcy4KCUZPUkVJR04gS0VZKENPTlRFTlRfSUQpIFJFRkVSRU5DRVMgY21zX2NvbnRlbnQoSUQpIE9OIERFTEVURSBDQVNDQURFLAoJRk9SRUlHTiBLRVkoQ09OVEVOVFRZUEVfVE9fVkFMVUVUWVBFX0lEKSBSRUZFUkVOQ0VTIGNtc19jb250ZW50dHlwZV90b192YWx1ZXR5cGUoSUQpIE9OIERFTEVURSBDQVNDQURFCik7CgpDUkVBVEUgVEFCTEUgY21zX3ZhbHVlX3N0cmluZ19zbWFsbCAoIAoJSUQgSU5URUdFUiBQUklNQVJZIEtFWSBBVVRPX0lOQ1JFTUVOVCwKCVZBTFVFIFZBUkNIQVIoMjU2KSBOT1QgTlVMTAopOwoKQ1JFQVRFIFRBQkxFIGNtc192YWx1ZV9zdHJpbmdfYmlnICgKCUlEIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT19JTkNSRU1FTlQsCglWQUxVRSBURVhUIE5PVCBOVUxMCik7CgpDUkVBVEUgVEFCTEUgY21zX3ZhbHVlX2RhdGUgKAoJSUQgSU5URUdFUiBQUklNQVJZIEtFWSBBVVRPX0lOQ1JFTUVOVCwKCVZBTFVFIERBVEUgTk9UIE5VTEwKKTsKCi0tIFRPRE86IFJlY29uc2lkZXIgdGhlc2UgT04gREVMRVRFIENBU0NBREVTIGFmdGVyIHRoaXMgcG9pbnQuCgpDUkVBVEUgVEFCTEUgY21zX3ZhbHVlX3JlZmVyZW5jZSAoCglJRCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9fSU5DUkVNRU5ULAoJVkFMVUUgSU5URUdFUiBOT1QgTlVMTCwKCUZPUkVJR04gS0VZKFZBTFVFKSBSRUZFUkVOQ0VTIGNtc19jb250ZW50KElEKSBPTiBERUxFVEUgQ0FTQ0FERQopOwoKQ1JFQVRFIFRBQkxFIGNtc192YWx1ZV9yZWZlcmVuY2VfbGlzdCAoCglJRCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9fSU5DUkVNRU5UCik7CgpDUkVBVEUgVEFCTEUgY21zX3ZhbHVlX3JlZmVyZW5jZV9saXN0X3ZhbHVlcyAoCglJRCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9fSU5DUkVNRU5ULAoJVkFMVUVfSUQgSU5URUdFUiBOT1QgTlVMTCwKCUNPTlRFTlRfSUQgSU5URUdFUiBOT1QgTlVMTCwKCUZPUkVJR04gS0VZKFZBTFVFX0lEKSBSRUZFUkVOQ0VTIGNtc192YWx1ZV9yZWZlcmVuY2VfbGlzdChJRCkgT04gREVMRVRFIENBU0NBREUsCglGT1JFSUdOIEtFWShDT05URU5UX0lEKSBSRUZFUkVOQ0VTIGNtc19jb250ZW50KElEKSBPTiBERUxFVEUgQ0FTQ0FERQopOwoKQ1JFQVRFIFRBQkxFIGNtc19ob29rcyAoCglJRCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9fSU5DUkVNRU5ULAoJVVJMIHZhcmNoYXIoMjU2KSBOT1QgTlVMTCwKCVNQQUNFX0lEIElOVEVHRVIgTk9UIE5VTEwsCglGT1JFSUdOIEtFWShTUEFDRV9JRCkgUkVGRVJFTkNFUyBjbXNfc3BhY2UoSUQpIE9OIERFTEVURSBDQVNDQURFLAoJQ09OU1RSQUlOVCBVTklRVUVQRVJDT05OIFVOSVFVRShTUEFDRV9JRCwgVVJMKQopOwo=")
+
+ migrations["sql/00002.sql"] = tostring("U0VMRUNUIDE7Cg==")
+
+ migrations["sql/00003.sql"] = tostring("Q1JFQVRFIFRBQkxFIGNtc19vcmcgKAoJSUQgSU5URUdFUiBQUklNQVJZIEtFWSBBVVRPX0lOQ1JFTUVOVCwKCUJPR1VTX1VTRVJfSUQgSU5URUdFUiBOT1QgTlVMTCwKCUNPTlNUUkFJTlQgQk9HVVNfVVNFUl9JRF9GSyBGT1JFSUdOIEtFWShCT0dVU19VU0VSX0lEKSBSRUZFUkVOQ0VTIGNtc191c2VyKElEKSBPTiBERUxFVEUgQ0FTQ0FERQopOwoKLS0gRml4IHVzZXIgdG8gb3JnLgoKQUxURVIgVEFCTEUgY21zX3VzZXIgQUREIE9SR19JRCBJTlRFR0VSIE5PVCBOVUxMOwoKLS0gTk9URTogQXQgdGhpcyBwb2ludCBpbiB0aW1lIHdlIGhhdmVuJ3Qgc3VwcG9ydGVkIHVzZXI8LT5zcGFjZS4KCklOU0VSVCBJTlRPIGNtc19vcmcgKEJPR1VTX1VTRVJfSUQpClNFTEVDVCBjbXNfdXNlci5JRCBGUk9NIGNtc191c2VyOwoKVVBEQVRFIGNtc191c2VyIApKT0lOIGNtc19vcmcgT04gY21zX3VzZXIuSUQ9Y21zX29yZy5CT0dVU19VU0VSX0lEClNFVCBjbXNfdXNlci5PUkdfSUQ9Y21zX29yZy5JRDsKCkFMVEVSIFRBQkxFIGNtc19vcmcgRFJPUCBGT1JFSUdOIEtFWSBCT0dVU19VU0VSX0lEX0ZLOwpBTFRFUiBUQUJMRSBjbXNfb3JnIERST1AgQ09MVU1OIEJPR1VTX1VTRVJfSUQ7CgpBTFRFUiBUQUJMRSBjbXNfdXNlciBBREQgQ09OU1RSQUlOVCBDTVNfVVNFUl9PUkdfSURfRksgRk9SRUlHTiBLRVkoT1JHX0lEKSBSRUZFUkVOQ0VTIGNtc19vcmcoSUQpOwoKLS0gRml4IHNwYWNlIHRvIG9yZy4KCkFMVEVSIFRBQkxFIGNtc19zcGFjZSBBREQgT1JHX0lEIElOVEVHRVIgTk9UIE5VTEw7CgpVUERBVEUgY21zX3NwYWNlCkpPSU4gY21zX3VzZXJfdG9fc3BhY2UgT04gU1BBQ0VfSUQ9Y21zX3NwYWNlLklEIApKT0lOIGNtc191c2VyIE9OIFVTRVJfSUQ9Y21zX3VzZXIuSUQKSk9JTiBjbXNfb3JnIE9OIGNtc191c2VyLk9SR19JRD1jbXNfb3JnLklEClNFVCBjbXNfc3BhY2UuT1JHX0lEPWNtc19vcmcuSUQ7CgpBTFRFUiBUQUJMRSBjbXNfc3BhY2UgQUREIENPTlNUUkFJTlQgQ01TX1NQQUNFX09SR19JRF9GSyBGT1JFSUdOIEtFWShPUkdfSUQpIFJFRkVSRU5DRVMgY21zX29yZyhJRCk7CgotLSBEcm9wIGV4dHJhcy4KCkRST1AgVEFCTEUgY21zX3VzZXJfdG9fc3BhY2U7Cg==")
+
+}
+
+func Get(name string) (string, bool) {
+ val, ok := migrations[name]
+ return val, ok
+}
+
+func Must(name string) string {
+ val, _ := migrations[name]
+ return val
+}
A internal/s/db/org.go => internal/s/db/org.go +12 -0
@@ 0,0 1,12 @@
+package db
+
+import (
+ "errors"
+
+ "git.sr.ht/~evanj/cms/internal/m/tier"
+ "git.sr.ht/~evanj/cms/internal/m/user"
+)
+
+func (db *DB) OrgGiveTier(u user.User, t tier.Tier) error {
+ return errors.New("no complete")
+}
M internal/s/db/space.go => internal/s/db/space.go +24 -25
@@ 21,17 21,23 @@ type Space struct {
}
var (
- queryCreateNewSpace = `INSERT INTO cms_space (NAME, DESCRIPTION) VALUES (?, ?);`
- queryUpdateSpace = `UPDATE cms_space SET NAME = ?, DESCRIPTION = ? WHERE ID = ?;`
- queryDeleteSpace = `DELETE cms_user_to_space, cms_space FROM cms_space JOIN cms_user_to_space ON cms_user_to_space.SPACE_ID = cms_space.ID WHERE SPACE_ID = ? AND USER_ID = ?;`
- queryCreateNewUserToSpace = `INSERT INTO cms_user_to_space (USER_ID, SPACE_ID) VALUES (?, ?);`
- queryFindSpaceByUserAndID = `SELECT cms_space.ID, cms_space.NAME, cms_space.DESCRIPTION FROM cms_space JOIN cms_user_to_space ON SPACE_ID=cms_space.ID WHERE USER_ID=? AND SPACE_ID=?;`
-
- copyUsersQuery = `
- INSERT INTO cms_user_to_space (USER_ID, SPACE_ID)
- SELECT USER_ID, ?
- FROM cms_user_to_space
- WHERE SPACE_ID=?
+ queryCreateNewSpace = `INSERT INTO cms_space (NAME, DESCRIPTION, ORG_ID) VALUES (?, ?, ?);`
+ queryUpdateSpace = `UPDATE cms_space SET NAME = ?, DESCRIPTION = ? WHERE ID = ?;`
+
+ queryDeleteSpace = `
+ DELETE cms_space FROM cms_space
+ JOIN cms_org ON cms_org.ID=cms_space.ORG_ID
+ JOIN cms_user ON cms_user.ORG_ID=cms_org.ID
+ WHERE cms_user.ID=?
+ AND cms_space.ID=?
+ `
+
+ queryFindSpaceByUserAndID = `
+ SELECT cms_space.ID, cms_space.NAME, cms_space.DESCRIPTION FROM cms_space
+ JOIN cms_org ON cms_org.ID=cms_space.ORG_ID
+ JOIN cms_user ON cms_user.ORG_ID=cms_org.ID
+ WHERE cms_user.ID=?
+ AND cms_space.ID=?
`
copyHooksQuery = `
@@ 57,7 63,7 @@ var (
)
func (db *DB) spaceNew(t *sql.Tx, user user.User, name, desc string) (space.Space, error) {
- res, err := t.Exec(queryCreateNewSpace, name, desc)
+ res, err := t.Exec(queryCreateNewSpace, name, desc, user.OrgID())
if err != nil {
return nil, fmt.Errorf("space '%s' already exists", name)
}
@@ 67,10 73,6 @@ func (db *DB) spaceNew(t *sql.Tx, user user.User, name, desc string) (space.Spac
return nil, fmt.Errorf("failed to create space")
}
- if _, err := t.Exec(queryCreateNewUserToSpace, user.ID(), id); err != nil {
- return nil, fmt.Errorf("failed to attach space to user")
- }
-
var space Space
if err := t.QueryRow(queryFindSpaceByUserAndID, user.ID(), id).Scan(&space.SpaceID, &space.SpaceName, &space.SpaceDesc); err != nil {
return nil, fmt.Errorf("failed to find space created")
@@ 131,7 133,7 @@ func (db *DB) SpaceCopy(user user.User, prevS space.Space, name, desc string) (s
}
defer t.Rollback()
- res, err := t.Exec(queryCreateNewSpace, name, desc)
+ res, err := t.Exec(queryCreateNewSpace, name, desc, user.OrgID())
if err != nil {
return nil, err
}
@@ 141,11 143,6 @@ func (db *DB) SpaceCopy(user user.User, prevS space.Space, name, desc string) (s
return nil, err
}
- // Copy all users.
- if _, err := t.Exec(copyUsersQuery, nextID, prevS.ID()); err != nil {
- return nil, err
- }
-
// Copy all webhooks.
if _, err := t.Exec(copyHooksQuery, nextID, prevS.ID()); err != nil {
return nil, err
@@ 393,9 390,11 @@ func (db *DB) spacesPerUser(t *sql.Tx, user user.User, before int) (space.SpaceL
before = beformat(before)
q := `
- SELECT SPACE_ID FROM cms_user_to_space
- WHERE USER_ID = ? AND SPACE_ID < ?
- ORDER BY SPACE_ID DESC LIMIT ?
+ SELECT cms_space.ID FROM cms_space
+ JOIN cms_org ON cms_org.ID=cms_space.ORG_ID
+ JOIN cms_user on cms_user.ORG_ID=cms_org.ID
+ WHERE cms_user.ID=? AND cms_space.ID<?
+ ORDER BY cms_space.ID DESC LIMIT ?
`
rows, err := db.Query(q, user.ID(), before, perPage+1)
M internal/s/db/sql/00001.sql => internal/s/db/sql/00001.sql +105 -0
@@ 0,0 1,105 @@
+CREATE TABLE cms_migrate (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ NAME varchar(256) UNIQUE NOT NULL,
+ DATE TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE cms_user (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ NAME varchar(256) UNIQUE NOT NULL,
+ HASH varchar(256) NOT NULL
+);
+
+CREATE TABLE cms_space (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ NAME varchar(256) NOT NULL,
+ DESCRIPTION varchar(256) NOT NULL
+);
+
+CREATE TABLE cms_user_to_space (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ USER_ID INTEGER NOT NULL,
+ SPACE_ID INTEGER NOT NULL,
+ FOREIGN KEY(USER_ID) REFERENCES cms_user(ID),
+ FOREIGN KEY(SPACE_ID) REFERENCES cms_space(ID) ON DELETE CASCADE
+);
+
+CREATE TABLE cms_contenttype (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ NAME varchar(256) NOT NULL,
+ SPACE_ID INTEGER NOT NULL,
+ CONSTRAINT FG FOREIGN KEY(SPACE_ID) REFERENCES cms_space(ID) ON DELETE CASCADE,
+ CONSTRAINT UNIQUEPERCONN UNIQUE(SPACE_ID, NAME)
+);
+
+CREATE TABLE cms_valuetype (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ VALUE varchar(256) UNIQUE NOT NULL
+);
+
+CREATE TABLE cms_contenttype_to_valuetype (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ NAME varchar(256) NOT NULL,
+ CONTENTTYPE_ID INTEGER NOT NULL,
+ VALUETYPE_ID INTEGER NOT NULL,
+ FOREIGN KEY(CONTENTTYPE_ID) REFERENCES cms_contenttype(ID) ON DELETE CASCADE,
+ FOREIGN KEY(VALUETYPE_ID) REFERENCES cms_valuetype(ID)
+);
+
+CREATE TABLE cms_content (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ CONTENTTYPE_ID INTEGER NOT NULL,
+ FOREIGN KEY(CONTENTTYPE_ID) REFERENCES cms_contenttype(ID) ON DELETE CASCADE
+);
+
+CREATE TABLE cms_value (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ CONTENT_ID INTEGER NOT NULL,
+ CONTENTTYPE_TO_VALUETYPE_ID INTEGER NOT NULL,
+ VALUE_ID INTEGER NOT NULL, -- Should be a foreign key but impossible to make it for two+ tables.
+ FOREIGN KEY(CONTENT_ID) REFERENCES cms_content(ID) ON DELETE CASCADE,
+ FOREIGN KEY(CONTENTTYPE_TO_VALUETYPE_ID) REFERENCES cms_contenttype_to_valuetype(ID) ON DELETE CASCADE
+);
+
+CREATE TABLE cms_value_string_small (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ VALUE VARCHAR(256) NOT NULL
+);
+
+CREATE TABLE cms_value_string_big (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ VALUE TEXT NOT NULL
+);
+
+CREATE TABLE cms_value_date (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ VALUE DATE NOT NULL
+);
+
+-- TODO: Reconsider these ON DELETE CASCADES after this point.
+
+CREATE TABLE cms_value_reference (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ VALUE INTEGER NOT NULL,
+ FOREIGN KEY(VALUE) REFERENCES cms_content(ID) ON DELETE CASCADE
+);
+
+CREATE TABLE cms_value_reference_list (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT
+);
+
+CREATE TABLE cms_value_reference_list_values (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ VALUE_ID INTEGER NOT NULL,
+ CONTENT_ID INTEGER NOT NULL,
+ FOREIGN KEY(VALUE_ID) REFERENCES cms_value_reference_list(ID) ON DELETE CASCADE,
+ FOREIGN KEY(CONTENT_ID) REFERENCES cms_content(ID) ON DELETE CASCADE
+);
+
+CREATE TABLE cms_hooks (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ URL varchar(256) NOT NULL,
+ SPACE_ID INTEGER NOT NULL,
+ FOREIGN KEY(SPACE_ID) REFERENCES cms_space(ID) ON DELETE CASCADE,
+ CONSTRAINT UNIQUEPERCONN UNIQUE(SPACE_ID, URL)
+);
A internal/s/db/sql/00002.sql => internal/s/db/sql/00002.sql +1 -0
A internal/s/db/sql/00003.sql => internal/s/db/sql/00003.sql +39 -0
@@ 0,0 1,39 @@
+CREATE TABLE cms_org (
+ ID INTEGER PRIMARY KEY AUTO_INCREMENT,
+ BOGUS_USER_ID INTEGER NOT NULL,
+ CONSTRAINT BOGUS_USER_ID_FK FOREIGN KEY(BOGUS_USER_ID) REFERENCES cms_user(ID) ON DELETE CASCADE
+);
+
+-- Fix user to org.
+
+ALTER TABLE cms_user ADD ORG_ID INTEGER NOT NULL;
+
+-- NOTE: At this point in time we haven't supported user<->space.
+
+INSERT INTO cms_org (BOGUS_USER_ID)
+SELECT cms_user.ID FROM cms_user;
+
+UPDATE cms_user
+JOIN cms_org ON cms_user.ID=cms_org.BOGUS_USER_ID
+SET cms_user.ORG_ID=cms_org.ID;
+
+ALTER TABLE cms_org DROP FOREIGN KEY BOGUS_USER_ID_FK;
+ALTER TABLE cms_org DROP COLUMN BOGUS_USER_ID;
+
+ALTER TABLE cms_user ADD CONSTRAINT CMS_USER_ORG_ID_FK FOREIGN KEY(ORG_ID) REFERENCES cms_org(ID);
+
+-- Fix space to org.
+
+ALTER TABLE cms_space ADD ORG_ID INTEGER NOT NULL;
+
+UPDATE cms_space
+JOIN cms_user_to_space ON SPACE_ID=cms_space.ID
+JOIN cms_user ON USER_ID=cms_user.ID
+JOIN cms_org ON cms_user.ORG_ID=cms_org.ID
+SET cms_space.ORG_ID=cms_org.ID;
+
+ALTER TABLE cms_space ADD CONSTRAINT CMS_SPACE_ORG_ID_FK FOREIGN KEY(ORG_ID) REFERENCES cms_org(ID);
+
+-- Drop extras.
+
+DROP TABLE cms_user_to_space;
M internal/s/db/user.go => internal/s/db/user.go +26 -11
@@ 9,9 9,10 @@ import (
type User struct {
// Stored in DB.
- UserID string
- UserName string
- userHash string
+ UserID string
+ UserName string
+ userHash string
+ userOrgID string
// Set on read.
userToken string
}
@@ 19,9 20,10 @@ type User struct {
// SQL QUERIES
var (
- queryCreateNewUser = `INSERT INTO cms_user (NAME, HASH) VALUES (?, ?);`
- queryFindUserByID = `SELECT ID, NAME, HASH FROM cms_user WHERE ID = ?;`
- queryFindUserByName = `SELECT ID, NAME, HASH FROM cms_user WHERE NAME = ?;`
+ queryCreateNewOrg = `INSERT INTO cms_org () VALUES ();`
+ queryCreateNewUser = `INSERT INTO cms_user (NAME, HASH, ORG_ID) VALUES (?, ?, ?);`
+ queryFindUserByID = `SELECT ID, NAME, HASH, ORG_ID FROM cms_user WHERE ID = ?;`
+ queryFindUserByName = `SELECT ID, NAME, HASH, ORG_ID FROM cms_user WHERE NAME = ?;`
)
func (db *DB) UserNew(username, password, verifyPassword string) (user.User, error) {
@@ 35,9 37,18 @@ func (db *DB) UserNew(username, password, verifyPassword string) (user.User, err
return nil, fmt.Errorf("failed to create password hash")
}
- res, err := db.Exec(queryCreateNewUser, username, hash)
+ res, err := db.Exec(queryCreateNewOrg)
+ if err != nil {
+ return nil, err // Fat chance.
+ }
+
+ orgID, err := res.LastInsertId()
+ if err != nil {
+ return nil, err // Fat chance.
+ }
+
+ res, err = db.Exec(queryCreateNewUser, username, hash, orgID)
if err != nil {
- db.log.Println(err)
return nil, fmt.Errorf("user '%s' already exists", username)
}
@@ 48,7 59,7 @@ func (db *DB) UserNew(username, password, verifyPassword string) (user.User, err
}
var user User
- if err := db.QueryRow(queryFindUserByID, id).Scan(&user.UserID, &user.UserName, &user.userHash); err != nil {
+ if err := db.QueryRow(queryFindUserByID, id).Scan(&user.UserID, &user.UserName, &user.userHash, &user.userOrgID); err != nil {
db.log.Println(err)
return nil, fmt.Errorf("failed to find user created")
}
@@ 65,7 76,7 @@ func (db *DB) UserNew(username, password, verifyPassword string) (user.User, err
func (db *DB) UserGet(username, password string) (user.User, error) {
var user User
- if err := db.QueryRow(queryFindUserByName, username).Scan(&user.UserID, &user.UserName, &user.userHash); err != nil {
+ if err := db.QueryRow(queryFindUserByName, username).Scan(&user.UserID, &user.UserName, &user.userHash, &user.userOrgID); err != nil {
db.log.Println(err)
return nil, fmt.Errorf("failed to find user '%s'", username)
}
@@ 98,7 109,7 @@ func (db *DB) UserGetFromToken(token string) (user.User, error) {
}
var user User
- if err := db.QueryRow(queryFindUserByID, id).Scan(&user.UserID, &user.UserName, &user.userHash); err != nil {
+ if err := db.QueryRow(queryFindUserByID, id).Scan(&user.UserID, &user.UserName, &user.userHash, &user.userOrgID); err != nil {
db.log.Println(err)
return nil, fmt.Errorf("failed to find user")
}
@@ 124,3 135,7 @@ func (u *User) Name() string {
func (u *User) Token() string {
return u.userToken
}
+
+func (u *User) OrgID() string {
+ return u.userOrgID
+}
M internal/s/stripe/stripe.go => internal/s/stripe/stripe.go +51 -35
@@ 1,41 1,39 @@
package stripe
import (
- "fmt"
-
"git.sr.ht/~evanj/cms/internal/m/tier"
"git.sr.ht/~evanj/cms/internal/m/user"
+ "github.com/pkg/errors"
lib "github.com/stripe/stripe-go/v71"
"github.com/stripe/stripe-go/v71/checkout/session"
"github.com/stripe/stripe-go/v71/customer"
)
-const (
- KeyUserToken = "StripeKeyCMSUserID"
-)
-
type Stripe struct {
sucesssURL, cancelURL string
pk, sk string
+ db DBer
}
-func New(sucesssURL, cancelURL, pk, sk string) Stripe {
+type DBer interface {
+ UserGetFromToken(token string) (user.User, error)
+ OrgGiveTier(user.User, tier.Tier) error
+}
+
+func New(sucesssURL, cancelURL, pk, sk string, db DBer) Stripe {
// Stripe sucks.
lib.Key = sk
return Stripe{
sucesssURL, cancelURL,
pk, sk,
+ db,
}
}
func (s Stripe) StartCheckout(user user.User, t tier.Tier) (string, string, error) {
customerParams := &lib.CustomerParams{
- Params: lib.Params{
- Metadata: map[string]string{
- KeyUserToken: user.Token(),
- },
- },
- Name: lib.String(user.Name()),
+ Name: lib.String(user.Name()),
+ Description: lib.String(user.Token()),
}
c, err := customer.New(customerParams)
@@ 55,7 53,7 @@ func (s Stripe) StartCheckout(user user.User, t tier.Tier) (string, string, erro
},
},
Mode: lib.String("subscription"),
- SuccessURL: lib.String(fmt.Sprintf("%s?session_id={CHECKOUT_SESSION_ID}", s.sucesssURL)),
+ SuccessURL: lib.String(s.sucesssURL),
CancelURL: lib.String(s.cancelURL),
}
@@ 67,24 65,42 @@ func (s Stripe) StartCheckout(user user.User, t tier.Tier) (string, string, erro
return sess.ID, s.pk, nil
}
-// func (s Stripe) CompleteCheckout(sessionID string) (tier.Tier, error) {
-// sess, _ := session.Get(sessionID, nil)
-// userID, ok := sess.Metadata[KeyUserToken]
-// if !ok {
-// return tier.Tier{}, errors.New("no user associated with transaction")
-// }
-//
-// i := session.ListLineItems(sessionID, nil)
-// for i.Next() {
-// li := i.LineItem()
-// if li.Price != nil {
-// t, ok := tier.ByStripePriceID(li.Price.ID)
-// if !ok {
-// return tier.Tier{}, errors.New("invalid stripe response: checkout not complete")
-// }
-// return t, nil
-// }
-// }
-//
-// return tier.Tier{}, errors.New("tier subscription could not be found")
-// }
+func (s Stripe) CompleteCheckout(sessionID string) error {
+ sess, err := session.Get(sessionID, &lib.CheckoutSessionParams{
+ PaymentIntentData: &lib.CheckoutSessionPaymentIntentDataParams{},
+ })
+ if err != nil {
+ return errors.Wrap(err, "transaction session not found")
+ }
+
+ c, err := customer.Get(sess.Customer.ID, nil)
+ if err != nil {
+ return errors.Wrap(err, "customer not found")
+ }
+
+ // NOTE: Only one subscription can exist at once per user/org.
+ var (
+ t tier.Tier
+ def tier.Tier
+ ok bool
+ )
+ for _, sub := range c.Subscriptions.Data {
+ for _, li := range sub.Items.Data {
+ t, ok = tier.ByStripePriceID(li.Price.ID)
+ if !ok {
+ return errors.New("mangled subscription item")
+ }
+ }
+ }
+
+ if t.Name == def.Name {
+ return errors.New("subscription could not be found")
+ }
+
+ user, err := s.db.UserGetFromToken(c.Description)
+ if err != nil {
+ return errors.Wrap(err, "mangled user token")
+ }
+
+ return s.db.OrgGiveTier(user, t)
+}
D internal/s/tmpl/tmpl.go => internal/s/tmpl/tmpl.go +0 -28
@@ 1,28 0,0 @@
-package tmpl
-
-import (
- "html/template"
- "strings"
-)
-
-//go:generate embed -pattern */* -id tmpls
-
-var all *template.Template
-
-func MustParse(name string) *template.Template {
- if all == nil {
-
- fns := template.FuncMap{
- "inc": func(i int) int { return i + 1 },
- "title": func(str string) string { return strings.Title(str) },
- }
-
- all = template.New("cms")
- for key, val := range tmpls {
- all = template.Must(all.New(key).Funcs(fns).Parse(val))
- }
-
- }
-
- return all.Lookup(name)
-}
D internal/s/tmpl/tmpl_test.go => internal/s/tmpl/tmpl_test.go +0 -1
@@ 1,1 0,0 @@
-package tmpl_test
R internal/s/tmpl/css/bootstrap.css => internal/v/css/bootstrap.css +0 -0
R internal/s/tmpl/css/main.css => internal/v/css/main.css +0 -0
R internal/s/tmpl/css/mvp.css => internal/v/css/mvp.css +0 -0
R => +0 -0
R internal/s/tmpl/html/_head.html => internal/v/html/_head.html +0 -0
R => +0 -0
R internal/s/tmpl/html/_scripts.html => internal/v/html/_scripts.html +0 -0
R internal/s/tmpl/html/billing.html => internal/v/html/billing.html +0 -0
R internal/s/tmpl/html/contact.html => internal/v/html/contact.html +0 -0
R internal/s/tmpl/html/content.html => internal/v/html/content.html +0 -0
R internal/s/tmpl/html/contenttype.html => internal/v/html/contenttype.html +0 -0
R internal/s/tmpl/html/doc.html => internal/v/html/doc.html +0 -0
R internal/s/tmpl/html/faq.html => internal/v/html/faq.html +0 -0
R internal/s/tmpl/html/hook.html => internal/v/html/hook.html +0 -0
R internal/s/tmpl/html/index.html => internal/v/html/index.html +0 -0
R internal/s/tmpl/html/privacy.html => internal/v/html/privacy.html +0 -0
R internal/s/tmpl/html/redirect.html => internal/v/html/redirect.html +0 -0
R internal/s/tmpl/html/space.html => internal/v/html/space.html +0 -0
R internal/s/tmpl/html/stripe.html => internal/v/html/stripe.html +0 -0
R internal/s/tmpl/html/terms.html => internal/v/html/terms.html +0 -0
R internal/s/tmpl/js/bootstrap.js => internal/v/js/bootstrap.js +0 -0
R internal/s/tmpl/js/content.js => internal/v/js/content.js +0 -0
R internal/s/tmpl/js/main.js => internal/v/js/main.js +0 -0
R internal/s/tmpl/js/popper.js => internal/v/js/popper.js +0 -0
R internal/s/tmpl/js/space.js => internal/v/js/space.js +0 -0
R internal/s/tmpl/tmpls_embed.go => internal/v/tmpls_embed.go +1 -1
@@ 1,6 1,6 @@
// Code generated by "embed -pattern */* -id tmpls"; DO NOT EDIT.
-package tmpl
+package v
import "encoding/base64"
M internal/v/v.go => internal/v/v.go +27 -0
@@ 1,1 1,28 @@
package v
+
+import (
+ "html/template"
+ "strings"
+)
+
+//go:generate embed -pattern */* -id tmpls
+
+var all *template.Template
+
+func MustParse(name string) *template.Template {
+ if all == nil {
+
+ fns := template.FuncMap{
+ "inc": func(i int) int { return i + 1 },
+ "title": func(str string) string { return strings.Title(str) },
+ }
+
+ all = template.New("cms")
+ for key, val := range tmpls {
+ all = template.Must(all.New(key).Funcs(fns).Parse(val))
+ }
+
+ }
+
+ return all.Lookup(name)
+}
M makefile => makefile +1 -1
@@ 37,4 37,4 @@ run: gen build
@env $(ENV) ./$(BIN)
dev:
- @find * -not -name '*_embed.go' | grep -E '*.(go|js|css|html)' | entr -r make run
+ @find * -not -name '*_embed.go' | grep -E '*.(sql|go|js|css|html)' | entr -r make run
D vendor/github.com/stripe/stripe-go/v71/webhook/client.go => vendor/github.com/stripe/stripe-go/v71/webhook/client.go +0 -228
@@ 1,228 0,0 @@
-package webhook
-
-import (
- "crypto/hmac"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "errors"
- "fmt"
- "strconv"
- "strings"
- "time"
-
- "github.com/stripe/stripe-go/v71"
-)
-
-//
-// Public constants
-//
-
-const (
- // DefaultTolerance indicates that signatures older than this will be rejected by ConstructEvent.
- DefaultTolerance time.Duration = 300 * time.Second
- // signingVersion represents the version of the signature we currently use.
- signingVersion string = "v1"
-)
-
-//
-// Public variables
-//
-
-// This block represents the list of errors that could be raised when using the webhook package.
-var (
- ErrInvalidHeader = errors.New("webhook has invalid Stripe-Signature header")
- ErrNoValidSignature = errors.New("webhook had no valid signature")
- ErrNotSigned = errors.New("webhook has no Stripe-Signature header")
- ErrTooOld = errors.New("timestamp wasn't within tolerance")
-)
-
-//
-// Public functions
-//
-
-// ComputeSignature computes a webhook signature using Stripe's v1 signing
-// method.
-//
-// See https://stripe.com/docs/webhooks#signatures for more information.
-func ComputeSignature(t time.Time, payload []byte, secret string) []byte {
- mac := hmac.New(sha256.New, []byte(secret))
- mac.Write([]byte(fmt.Sprintf("%d", t.Unix())))
- mac.Write([]byte("."))
- mac.Write(payload)
- return mac.Sum(nil)
-}
-
-// ConstructEvent initializes an Event object from a JSON webhook payload, validating
-// the Stripe-Signature header using the specified signing secret. Returns an error
-// if the body or Stripe-Signature header provided are unreadable, if the
-// signature doesn't match, or if the timestamp for the signature is older than
-// DefaultTolerance.
-//
-// NOTE: Stripe will only send Webhook signing headers after you have retrieved
-// your signing secret from the Stripe dashboard:
-// https://dashboard.stripe.com/webhooks
-//
-func ConstructEvent(payload []byte, header string, secret string) (stripe.Event, error) {
- return ConstructEventWithTolerance(payload, header, secret, DefaultTolerance)
-}
-
-// ConstructEventIgnoringTolerance initializes an Event object from a JSON webhook
-// payload, validating the Stripe-Signature header using the specified signing secret.
-// Returns an error if the body or Stripe-Signature header provided are unreadable or
-// if the signature doesn't match. Does not check the signature's timestamp.
-//
-// NOTE: Stripe will only send Webhook signing headers after you have retrieved
-// your signing secret from the Stripe dashboard:
-// https://dashboard.stripe.com/webhooks
-//
-func ConstructEventIgnoringTolerance(payload []byte, header string, secret string) (stripe.Event, error) {
- return constructEvent(payload, header, secret, 0*time.Second, false)
-}
-
-// ConstructEventWithTolerance initializes an Event object from a JSON webhook payload,
-// validating the signature in the Stripe-Signature header using the specified signing
-// secret and tolerance window. Returns an error if the body or Stripe-Signature header
-// provided are unreadable, if the signature doesn't match, or if the timestamp
-// for the signature is older than the specified tolerance.
-//
-// NOTE: Stripe will only send Webhook signing headers after you have retrieved
-// your signing secret from the Stripe dashboard:
-// https://dashboard.stripe.com/webhooks
-//
-func ConstructEventWithTolerance(payload []byte, header string, secret string, tolerance time.Duration) (stripe.Event, error) {
- return constructEvent(payload, header, secret, tolerance, true)
-}
-
-// ValidatePayload validates the payload against the Stripe-Signature header
-// using the specified signing secret. Returns an error if the body or
-// Stripe-Signature header provided are unreadable, if the signature doesn't
-// match, or if the timestamp for the signature is older than DefaultTolerance.
-//
-// NOTE: Stripe will only send Webhook signing headers after you have retrieved
-// your signing secret from the Stripe dashboard:
-// https://dashboard.stripe.com/webhooks
-//
-func ValidatePayload(payload []byte, header string, secret string) error {
- return ValidatePayloadWithTolerance(payload, header, secret, DefaultTolerance)
-}
-
-// ValidatePayloadIgnoringTolerance validates the payload against the Stripe-Signature header
-// header using the specified signing secret. Returns an error if the body or
-// Stripe-Signature header provided are unreadable or if the signature doesn't match.
-// Does not check the signature's timestamp.
-//
-// NOTE: Stripe will only send Webhook signing headers after you have retrieved
-// your signing secret from the Stripe dashboard:
-// https://dashboard.stripe.com/webhooks
-//
-func ValidatePayloadIgnoringTolerance(payload []byte, header string, secret string) error {
- return validatePayload(payload, header, secret, 0*time.Second, false)
-}
-
-// ValidatePayloadWithTolerance validates the payload against the Stripe-Signature header
-// using the specified signing secret and tolerance window. Returns an error if the body
-// or Stripe-Signature header provided are unreadable, if the signature doesn't match, or
-// if the timestamp for the signature is older than the specified tolerance.
-//
-// NOTE: Stripe will only send Webhook signing headers after you have retrieved
-// your signing secret from the Stripe dashboard:
-// https://dashboard.stripe.com/webhooks
-//
-func ValidatePayloadWithTolerance(payload []byte, header string, secret string, tolerance time.Duration) error {
- return validatePayload(payload, header, secret, tolerance, true)
-}
-
-//
-// Private types
-//
-
-type signedHeader struct {
- timestamp time.Time
- signatures [][]byte
-}
-
-//
-// Private functions
-//
-
-func constructEvent(payload []byte, sigHeader string, secret string, tolerance time.Duration, enforceTolerance bool) (stripe.Event, error) {
- e := stripe.Event{}
-
- if err := validatePayload(payload, sigHeader, secret, tolerance, enforceTolerance); err != nil {
- return e, err
- }
-
- if err := json.Unmarshal(payload, &e); err != nil {
- return e, fmt.Errorf("Failed to parse webhook body json: %s", err.Error())
- }
-
- return e, nil
-
-}
-
-func parseSignatureHeader(header string) (*signedHeader, error) {
- sh := &signedHeader{}
-
- if header == "" {
- return sh, ErrNotSigned
- }
-
- // Signed header looks like "t=1495999758,v1=ABC,v1=DEF,v0=GHI"
- pairs := strings.Split(header, ",")
- for _, pair := range pairs {
- parts := strings.Split(pair, "=")
- if len(parts) != 2 {
- return sh, ErrInvalidHeader
- }
-
- switch parts[0] {
- case "t":
- timestamp, err := strconv.ParseInt(parts[1], 10, 64)
- if err != nil {
- return sh, ErrInvalidHeader
- }
- sh.timestamp = time.Unix(timestamp, 0)
-
- case signingVersion:
- sig, err := hex.DecodeString(parts[1])
- if err != nil {
- continue // Ignore invalid signatures
- }
-
- sh.signatures = append(sh.signatures, sig)
-
- default:
- continue // Ignore unknown parts of the header
- }
- }
-
- if len(sh.signatures) == 0 {
- return sh, ErrNoValidSignature
- }
-
- return sh, nil
-}
-
-func validatePayload(payload []byte, sigHeader string, secret string, tolerance time.Duration, enforceTolerance bool) error {
-
- header, err := parseSignatureHeader(sigHeader)
- if err != nil {
- return err
- }
-
- expectedSignature := ComputeSignature(header.timestamp, payload, secret)
- expiredTimestamp := time.Since(header.timestamp) > tolerance
- if enforceTolerance && expiredTimestamp {
- return ErrTooOld
- }
-
- // Check all given v1 signatures, multiple signatures will be sent temporarily in the case of a rolled signature secret
- for _, sig := range header.signatures {
- if hmac.Equal(expectedSignature, sig) {
- return nil
- }
- }
-
- return ErrNoValidSignature
-}
M vendor/modules.txt => vendor/modules.txt +0 -1
@@ 38,7 38,6 @@ github.com/stripe/stripe-go/v71/checkout/session
github.com/stripe/stripe-go/v71/customer
github.com/stripe/stripe-go/v71/form
github.com/stripe/stripe-go/v71/lineitem
-github.com/stripe/stripe-go/v71/webhook
# golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d
golang.org/x/crypto/bcrypt
golang.org/x/crypto/blowfish