ducky/subscriptions
ducky/subscriptions/api/subscription_handlers.go
Return errors from responses in client. When the client makes a request, non-200 responses _are not_ considered errors. So we need to check the response.Errors property, and if it has errors, _then_ we consider the request to have an error. To make this happen, we created an httpErrors type that fulfills the error interface and just wraps the response Errors property. Then callers can type-cast it and interrogate it.
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 = 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."}
19 )
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))))
28 }
30 func CreateSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
31 store, err := getSubscriptionStore(c)
32 if err != nil {
33 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
34 return
35 }
36 stripe, err := getStripeClient(c)
37 if err != nil {
38 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
39 return
40 }
41 if !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
42 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
43 return
44 }
45 var req subscriptions.SubscriptionChange
46 err = api.Decode(r, &req)
47 if err != nil {
48 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
49 return
50 }
51 // BUG(paddy): Need to validate the request when creating a subscription
52 sub, err := subscriptions.New(req, stripe, store)
53 if err != nil {
54 var rErr api.RequestError
55 var code int
56 switch err {
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
63 default:
64 rErr = api.RequestError{Slug: api.RequestErrActOfGod}
65 code = http.StatusInternalServerError
66 }
67 api.Encode(w, r, code, Response{Errors: []api.RequestError{rErr}})
68 return
69 }
70 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
71 api.Encode(w, r, http.StatusCreated, resp)
72 }
74 func GetSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
75 store, err := getSubscriptionStore(c)
76 if err != nil {
77 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
78 return
79 }
80 vars := trout.RequestVars(r)
81 rawID := vars.Get("id")
82 if rawID == "" {
83 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
84 return
85 }
86 id, err := uuid.Parse(rawID)
87 if err != nil {
88 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
89 return
90 }
91 if !api.CheckScopes(r, ScopeSubscription.ID) {
92 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
93 return
94 }
95 userID, err := api.AuthUser(r)
96 if err != nil {
97 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
98 return
99 }
100 if !id.Equal(userID) && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
101 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
102 return
103 }
104 subs, err := store.GetSubscriptions([]uuid.ID{id})
105 if err != nil {
106 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
107 return
108 }
109 sub, ok := subs[id.String()]
110 if !ok {
111 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
112 return
113 }
114 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
115 api.Encode(w, r, http.StatusOK, resp)
116 }
118 func PatchSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
119 store, err := getSubscriptionStore(c)
120 if err != nil {
121 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
122 return
123 }
124 stripe, err := getStripeClient(c)
125 if err != nil {
126 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
127 return
128 }
129 if !api.CheckScopes(r, ScopeSubscription.ID) {
130 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
131 return
132 }
133 userID, err := api.AuthUser(r)
134 if err != nil {
135 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
136 return
137 }
138 vars := trout.RequestVars(r)
139 rawID := vars.Get("id")
140 if rawID == "" {
141 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
142 return
143 }
144 id, err := uuid.Parse(rawID)
145 if err != nil {
146 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
147 return
148 }
150 var req subscriptions.SubscriptionChange
151 err = api.Decode(r, &req)
152 if err != nil {
153 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
154 return
155 }
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}
164 }
165 api.Encode(w, r, http.StatusBadRequest, Response{Errors: errs})
166 return
167 }
169 subs, err := store.GetSubscriptions([]uuid.ID{userID})
170 if err != nil {
171 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
172 return
173 }
174 sub, ok := subs[userID.String()]
175 if !ok {
176 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
177 return
178 }
179 stripeSub, err := subscriptions.UpdateStripeSubscription(sub.StripeSubscription, req.Plan, req.StripeSource, stripe)
180 if err != nil {
181 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
182 return
183 }
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)
188 return
189 }
190 err = store.UpdateSubscription(id, change)
191 if err != nil {
192 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
193 return
194 }
195 sub.ApplyChange(change)
196 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
197 api.Encode(w, r, http.StatusOK, resp)
198 }