Create a listener that will create subscriptions.
We need a listener (as discussed in c4cfceb2f2fb) that will create a
Subscription whenever an auth.Profile is created. This is the beginning of that
effort. It hasn't been tested, and all the pieces aren't in place, but it's a
rough skeleton.
We have a Dockerfile that will correctly build a minimal container for the
listener.
We have a build-docker.sh script that will correctly build a binary that will be
used in the Dockerfile.
We have a ca-certificates.crt, which are pulled from Ubuntu, and are necessary
before we can safely consume SSL endpoints.
We created a small consumer script that listens for events off NSQ, and calls
the appropriate endpoint for our Subscriptions API. This is untested, and it
doesn't build at the moment, but that's awaiting changes in the
code.secondbit.org/auth.hg package.
Finally, we have a wrapper.sh file that will expose the Stripe secret key being
used from a Kubernetes secret file as an environment variable, instead.
6 "code.secondbit.org/api.hg"
7 "code.secondbit.org/auth.hg"
8 "code.secondbit.org/trout.hg"
9 "code.secondbit.org/uuid.hg"
11 "code.secondbit.org/ducky/subscriptions.hg"
13 "golang.org/x/net/context"
17 ScopeSubscription = auth.Scope{ID: "subscriptions", Name: "Manage Subscriptions", Description: "Read and update your subscription information."}
18 ScopeSubscriptionAdmin = auth.Scope{ID: "subscriptions_admin", Name: "Administer Subscriptions", Description: "Read and update subscription information, bypassing ACL."}
21 func HandleSubscriptions(router *trout.Router, c context.Context) {
22 router.Endpoint("/subscriptions").Methods("POST", "OPTIONS").Handler(
23 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, CreateSubscriptionHandler))))
24 router.Endpoint("/subscriptions/{id}").Methods("GET", "OPTIONS").Handler(
25 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, GetSubscriptionHandler))))
26 router.Endpoint("/subscriptions/{id}").Methods("PATCH", "OPTIONS").Handler(
27 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, PatchSubscriptionHandler))))
30 func CreateSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
31 store, err := getSubscriptionStore(c)
33 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
36 stripe, err := getStripeClient(c)
38 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
41 if !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
42 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
45 var req subscriptions.SubscriptionChange
46 err = api.Decode(r, &req)
48 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
51 // BUG(paddy): Need to validate the request when creating a subscription
52 sub, err := subscriptions.New(req, stripe, store)
54 var rErr api.RequestError
57 case subscriptions.ErrSubscriptionAlreadyExists:
58 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/user_id"}
59 code = http.StatusBadRequest
60 case subscriptions.ErrStripeSubscriptionAlreadyExists:
61 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/stripe_token"}
62 code = http.StatusBadRequest
64 rErr = api.RequestError{Slug: api.RequestErrActOfGod}
65 code = http.StatusInternalServerError
67 api.Encode(w, r, code, Response{Errors: []api.RequestError{rErr}})
70 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
71 api.Encode(w, r, http.StatusCreated, resp)
74 func GetSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
75 store, err := getSubscriptionStore(c)
77 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
80 vars := trout.RequestVars(r)
81 rawID := vars.Get("id")
83 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
86 id, err := uuid.Parse(rawID)
88 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
91 if !api.CheckScopes(r, ScopeSubscription.ID) {
92 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
95 userID, err := api.AuthUser(r)
97 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
100 if !id.Equal(userID) && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
101 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
104 subs, err := store.GetSubscriptions([]uuid.ID{id})
106 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
109 sub, ok := subs[id.String()]
111 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
114 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
115 api.Encode(w, r, http.StatusOK, resp)
118 func PatchSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
119 store, err := getSubscriptionStore(c)
121 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
124 stripe, err := getStripeClient(c)
126 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
129 if !api.CheckScopes(r, ScopeSubscription.ID) {
130 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
133 userID, err := api.AuthUser(r)
135 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
138 vars := trout.RequestVars(r)
139 rawID := vars.Get("id")
141 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
144 id, err := uuid.Parse(rawID)
146 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
150 var req subscriptions.SubscriptionChange
151 err = api.Decode(r, &req)
153 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
156 // BUG(paddy): Need to validate the request when updating a subscription
158 // only admin users can update the system-controlled properties
159 changedSysProps := subscriptions.ChangingSystemProperties(req)
160 if len(changedSysProps) > 0 && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
161 errs := make([]api.RequestError, len(changedSysProps))
162 for pos, prop := range changedSysProps {
163 errs[pos] = api.RequestError{Slug: api.RequestErrAccessDenied, Field: prop}
165 api.Encode(w, r, http.StatusBadRequest, Response{Errors: errs})
169 subs, err := store.GetSubscriptions([]uuid.ID{userID})
171 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
174 sub, ok := subs[userID.String()]
176 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
179 stripeSub, err := subscriptions.UpdateStripeSubscription(sub.StripeSubscription, req.Plan, req.StripeSource, stripe)
181 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
184 change := subscriptions.StripeSubscriptionChange(sub, *stripeSub)
185 if change.IsEmpty() {
186 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
187 api.Encode(w, r, http.StatusOK, resp)
190 err = store.UpdateSubscription(id, change)
192 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
195 sub.ApplyChange(change)
196 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
197 api.Encode(w, r, http.StatusOK, resp)