ducky/subscriptions

Paddy 2015-07-18 Parent:c4cfceb2f2fb Child:0ae1ff0ee306

9:8eb19bcbf17d Go to Latest

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.

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@6 17 ScopeSubscription = auth.Scope{ID: "subscriptions", Name: "Manage Subscriptions", Description: "Read and update your subscription information."}
paddy@6 18 ScopeSubscriptionAdmin = auth.Scope{ID: "subscriptions_admin", Name: "Administer Subscriptions", Description: "Read and update subscription information, bypassing ACL."}
paddy@4 19 )
paddy@4 20
paddy@4 21 func HandleSubscriptions(router *trout.Router, c context.Context) {
paddy@4 22 router.Endpoint("/subscriptions").Methods("POST", "OPTIONS").Handler(
paddy@4 23 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, CreateSubscriptionHandler))))
paddy@4 24 router.Endpoint("/subscriptions/{id}").Methods("GET", "OPTIONS").Handler(
paddy@4 25 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, GetSubscriptionHandler))))
paddy@6 26 router.Endpoint("/subscriptions/{id}").Methods("PATCH", "OPTIONS").Handler(
paddy@6 27 api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, PatchSubscriptionHandler))))
paddy@4 28 }
paddy@4 29
paddy@4 30 func CreateSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
paddy@4 31 store, err := getSubscriptionStore(c)
paddy@4 32 if err != nil {
paddy@4 33 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
paddy@4 34 return
paddy@4 35 }
paddy@4 36 stripe, err := getStripeClient(c)
paddy@4 37 if err != nil {
paddy@4 38 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
paddy@4 39 return
paddy@4 40 }
paddy@6 41 if !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
paddy@4 42 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
paddy@4 43 return
paddy@4 44 }
paddy@6 45 var req subscriptions.SubscriptionChange
paddy@4 46 err = api.Decode(r, &req)
paddy@4 47 if err != nil {
paddy@4 48 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
paddy@4 49 return
paddy@4 50 }
paddy@6 51 // BUG(paddy): Need to validate the request when creating a subscription
paddy@4 52 sub, err := subscriptions.New(req, stripe, store)
paddy@4 53 if err != nil {
paddy@4 54 var rErr api.RequestError
paddy@4 55 var code int
paddy@4 56 switch err {
paddy@4 57 case subscriptions.ErrSubscriptionAlreadyExists:
paddy@4 58 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/user_id"}
paddy@4 59 code = http.StatusBadRequest
paddy@4 60 case subscriptions.ErrStripeSubscriptionAlreadyExists:
paddy@4 61 rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/stripe_token"}
paddy@4 62 code = http.StatusBadRequest
paddy@4 63 default:
paddy@4 64 rErr = api.RequestError{Slug: api.RequestErrActOfGod}
paddy@4 65 code = http.StatusInternalServerError
paddy@4 66 }
paddy@4 67 api.Encode(w, r, code, Response{Errors: []api.RequestError{rErr}})
paddy@4 68 return
paddy@4 69 }
paddy@4 70 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
paddy@4 71 api.Encode(w, r, http.StatusCreated, resp)
paddy@4 72 }
paddy@4 73
paddy@4 74 func GetSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
paddy@4 75 store, err := getSubscriptionStore(c)
paddy@4 76 if err != nil {
paddy@4 77 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
paddy@4 78 return
paddy@4 79 }
paddy@4 80 vars := trout.RequestVars(r)
paddy@4 81 rawID := vars.Get("id")
paddy@4 82 if rawID == "" {
paddy@4 83 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
paddy@4 84 return
paddy@4 85 }
paddy@4 86 id, err := uuid.Parse(rawID)
paddy@4 87 if err != nil {
paddy@4 88 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
paddy@4 89 return
paddy@4 90 }
paddy@6 91 if !api.CheckScopes(r, ScopeSubscription.ID) {
paddy@4 92 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
paddy@4 93 return
paddy@4 94 }
paddy@4 95 userID, err := api.AuthUser(r)
paddy@4 96 if err != nil {
paddy@4 97 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
paddy@4 98 return
paddy@4 99 }
paddy@6 100 if !id.Equal(userID) && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
paddy@4 101 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
paddy@4 102 return
paddy@4 103 }
paddy@4 104 subs, err := store.GetSubscriptions([]uuid.ID{id})
paddy@4 105 if err != nil {
paddy@4 106 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
paddy@4 107 return
paddy@4 108 }
paddy@4 109 sub, ok := subs[id.String()]
paddy@4 110 if !ok {
paddy@4 111 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
paddy@4 112 return
paddy@4 113 }
paddy@4 114 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
paddy@4 115 api.Encode(w, r, http.StatusOK, resp)
paddy@4 116 }
paddy@6 117
paddy@6 118 func PatchSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) {
paddy@6 119 store, err := getSubscriptionStore(c)
paddy@6 120 if err != nil {
paddy@6 121 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
paddy@6 122 return
paddy@6 123 }
paddy@6 124 stripe, err := getStripeClient(c)
paddy@6 125 if err != nil {
paddy@6 126 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
paddy@6 127 return
paddy@6 128 }
paddy@6 129 if !api.CheckScopes(r, ScopeSubscription.ID) {
paddy@6 130 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
paddy@6 131 return
paddy@6 132 }
paddy@6 133 userID, err := api.AuthUser(r)
paddy@6 134 if err != nil {
paddy@6 135 api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}})
paddy@6 136 return
paddy@6 137 }
paddy@6 138 vars := trout.RequestVars(r)
paddy@6 139 rawID := vars.Get("id")
paddy@6 140 if rawID == "" {
paddy@6 141 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}})
paddy@6 142 return
paddy@6 143 }
paddy@6 144 id, err := uuid.Parse(rawID)
paddy@6 145 if err != nil {
paddy@6 146 api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
paddy@6 147 return
paddy@6 148 }
paddy@6 149
paddy@6 150 var req subscriptions.SubscriptionChange
paddy@6 151 err = api.Decode(r, &req)
paddy@6 152 if err != nil {
paddy@6 153 api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
paddy@6 154 return
paddy@6 155 }
paddy@6 156 // BUG(paddy): Need to validate the request when updating a subscription
paddy@6 157
paddy@6 158 // only admin users can update the system-controlled properties
paddy@6 159 changedSysProps := subscriptions.ChangingSystemProperties(req)
paddy@6 160 if len(changedSysProps) > 0 && !api.CheckScopes(r, ScopeSubscriptionAdmin.ID) {
paddy@6 161 errs := make([]api.RequestError, len(changedSysProps))
paddy@6 162 for pos, prop := range changedSysProps {
paddy@6 163 errs[pos] = api.RequestError{Slug: api.RequestErrAccessDenied, Field: prop}
paddy@6 164 }
paddy@6 165 api.Encode(w, r, http.StatusBadRequest, Response{Errors: errs})
paddy@6 166 return
paddy@6 167 }
paddy@6 168
paddy@6 169 subs, err := store.GetSubscriptions([]uuid.ID{userID})
paddy@6 170 if err != nil {
paddy@6 171 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
paddy@6 172 return
paddy@6 173 }
paddy@6 174 sub, ok := subs[userID.String()]
paddy@6 175 if !ok {
paddy@6 176 api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
paddy@6 177 return
paddy@6 178 }
paddy@6 179 stripeSub, err := subscriptions.UpdateStripeSubscription(sub.StripeSubscription, req.Plan, req.StripeSource, stripe)
paddy@6 180 if err != nil {
paddy@6 181 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
paddy@6 182 return
paddy@6 183 }
paddy@6 184 change := subscriptions.StripeSubscriptionChange(sub, *stripeSub)
paddy@6 185 if change.IsEmpty() {
paddy@6 186 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
paddy@6 187 api.Encode(w, r, http.StatusOK, resp)
paddy@6 188 return
paddy@6 189 }
paddy@6 190 err = store.UpdateSubscription(id, change)
paddy@6 191 if err != nil {
paddy@6 192 api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
paddy@6 193 return
paddy@6 194 }
paddy@6 195 sub.ApplyChange(change)
paddy@6 196 resp := Response{Subscriptions: []subscriptions.Subscription{sub}}
paddy@6 197 api.Encode(w, r, http.StatusOK, resp)
paddy@6 198 }