ducky/subscriptions

Paddy 2015-10-04 Parent:b063bc0a6e84

17:7eef47ecc01c Go to Latest

ducky/subscriptions/api/subscription_handlers.go

Document our client to make golint happy. Take care of all the documentation warnings in the client subpackage, which means golint now returns successfully.

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 }