ducky/subscriptions

Paddy 2015-10-04 Parent:0ae1ff0ee306

16:b063bc0a6e84 Go to Latest

ducky/subscriptions/api/subscription_handlers.go

Make api subpackage golint-passing. Add comments to all the exported functions, methods, and variables in the api subpackage, to make golint happy. Also, make the individual endpoints in the api subpackage unexported, as there's no real use case for exporting them. The handlers depend on the placeholders in the endpoint, so we need them to be controlled in unison, which means it's probably a bad idea to declare the route outside of the API package. And the only reason to expose the Handler is so people can declare custom endpoints.

History
1 package api
3 import (
4 "net/http"
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"
14 )
16 var (
17 // ScopeSubscription is an auth.Scope that controls access to
18 // subscriptions.
19 ScopeSubscription = auth.Scope{
20 ID: "subscriptions",
21 Name: "Manage Subscriptions",
22 Description: "Read and update your subscription information.",
23 }
24 // ScopeSubscriptionAdmin is an auth.Scope that controls access
25 // to subscriptions, bypassing the ACL to achieve administrator
26 // access.
27 ScopeSubscriptionAdmin = auth.Scope{ID: "subscriptions_admin", Name: "Administer Subscriptions", Description: "Read and update subscription information, bypassing ACL."}
28 )
30 // changingSystemProperties takes a SubscriptionChange and returns a
31 // slice of JSON pointers to the properties that are changing but are
32 // also designated as "system properties" in the API. If no properties
33 // meet these criteria, an empty slice is returned.
34 func changingSystemProperties(change subscriptions.SubscriptionChange) []string {
35 var changes []string
36 if change.StripeSubscription != nil {
37 changes = append(changes, "/stripe_subscription")
38 }
39 if change.Status != nil {
40 changes = append(changes, "/status")
41 }
42 if change.TrialStart != nil {
43 changes = append(changes, "/trial_start")
44 }
45 if change.TrialEnd != nil {
46 changes = append(changes, "/trial_end")
47 }
48 if change.PeriodStart != nil {
49 changes = append(changes, "/period_start")
50 }
51 if change.PeriodEnd != nil {
52 changes = append(changes, "/period_end")
53 }
54 if change.CanceledAt != nil {
55 changes = append(changes, "/canceled_at")
56 }
57 if change.FailedChargeAttempts != nil {
58 changes = append(changes, "/failed_charge_attempts")
59 }
60 if change.LastFailedCharge != nil {
61 changes = append(changes, "/last_failed_charge")
62 }
63 if change.LastNotified != nil {
64 changes = append(changes, "/last_notified")
65 }
66 return changes
67 }
69 // HandleSubscriptions registers the endpoints for the subscriptions API
70 // with the passed router. the passed Context will be passed to all the
71 // http.Handlers for the endpoints.
72 func HandleSubscriptions(router *trout.Router, c context.Context) {
73 router.Endpoint("/subscriptions").Methods("POST", "OPTIONS").Handler(
74 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, createSubscriptionHandler))))
75 router.Endpoint("/subscriptions/{id}").Methods("GET", "OPTIONS").Handler(
76 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, getSubscriptionHandler))))
77 router.Endpoint("/subscriptions/{id}").Methods("PATCH", "OPTIONS").Handler(
78 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, patchSubscriptionHandler))))
79 }
81 func createSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
82 store, err := getSubscriptionStore(c)
83 if err != nil {
84 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
85 return
86 }
87 stripe, err := getStripeClient(c)
88 if err != nil {
89 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
90 return
91 }
92 if !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
93 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
94 return
95 }
96 var req subscriptions.SubscriptionChange
97 err = api.Decode(r, &req)
98 if err != nil {
99 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
100 return
101 }
102 // BUG(paddy): Need to validate the request when creating a subscription
103 sub, err := subscriptions.New(req, stripe, store)
104 if err != nil {
105 var rErr api.RequestError
106 var code int
107 switch err {
108 case subscriptions.ErrSubscriptionAlreadyExists:
109 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/user_id"}
110 code = http.StatusBadRequest
111 case subscriptions.ErrStripeSubscriptionAlreadyExists:
112 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/stripe_token"}
113 code = http.StatusBadRequest
114 default:
115 rErr = api.RequestError{Slug: api.RequestErrActOfGod}
116 code = http.StatusInternalServerError
117 }
118 api.Encode(w, r, code, Response{Errors: []api.RequestError{rErr}})
119 return
120 }
121 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
122 api.Encode(w, r, http.StatusCreated, resp)
123 }
125 func getSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
126 store, err := getSubscriptionStore(c)
127 if err != nil {
128 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
129 return
130 }
131 vars := trout.RequestVars(r)
132 rawID := vars.Get("id")
133 if rawID == "" {
134 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
135 return
136 }
137 id, err := uuid.Parse(rawID)
138 if err != nil {
139 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
140 return
141 }
142 if !api.CheckScopes(r, ScopeSubscription.ID) {
143 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
144 return
145 }
146 userID, err := api.AuthUser(r)
147 if err != nil {
148 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
149 return
150 }
151 if !id.Equal(userID) && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
152 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
153 return
154 }
155 subs, err := store.GetSubscriptions([]uuid.ID{id})
156 if err != nil {
157 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
158 return
159 }
160 sub, ok := subs[id.String()]
161 if !ok {
162 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
163 return
164 }
165 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
166 api.Encode(w, r, http.StatusOK, resp)
167 }
169 func patchSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
170 store, err := getSubscriptionStore(c)
171 if err != nil {
172 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
173 return
174 }
175 stripe, err := getStripeClient(c)
176 if err != nil {
177 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
178 return
179 }
180 if !api.CheckScopes(r, ScopeSubscription.ID) {
181 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
182 return
183 }
184 userID, err := api.AuthUser(r)
185 if err != nil {
186 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
187 return
188 }
189 vars := trout.RequestVars(r)
190 rawID := vars.Get("id")
191 if rawID == "" {
192 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
193 return
194 }
195 id, err := uuid.Parse(rawID)
196 if err != nil {
197 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
198 return
199 }
201 var req subscriptions.SubscriptionChange
202 err = api.Decode(r, &req)
203 if err != nil {
204 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
205 return
206 }
207 // BUG(paddy): Need to validate the request when updating a subscription
209 // only admin users can update the system-controlled properties
210 changedSysProps := changingSystemProperties(req)
211 if len(changedSysProps) > 0 && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
212 errs := make([]api.RequestError, len(changedSysProps))
213 for pos, prop := range changedSysProps {
214 errs[pos] = api.RequestError{Slug: api.RequestErrAccessDenied, Field: prop}
215 }
216 api.Encode(w, r, http.StatusBadRequest, Response{Errors: errs})
217 return
218 }
220 subs, err := store.GetSubscriptions([]uuid.ID{userID})
221 if err != nil {
222 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
223 return
224 }
225 sub, ok := subs[userID.String()]
226 if !ok {
227 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
228 return
229 }
230 stripeSub, err := subscriptions.UpdateStripeSubscription(sub.StripeSubscription, req.Plan, req.StripeSource, stripe)
231 if err != nil {
232 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
233 return
234 }
235 change := subscriptions.StripeSubscriptionChange(sub, *stripeSub)
236 if change.IsEmpty() {
237 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
238 api.Encode(w, r, http.StatusOK, resp)
239 return
240 }
241 err = store.UpdateSubscription(id, change)
242 if err != nil {
243 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
244 return
245 }
246 sub.ApplyChange(change)
247 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
248 api.Encode(w, r, http.StatusOK, resp)
249 }