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