ducky/subscriptions
ducky/subscriptions/api/subscription_handlers.go
Create a listener that will create subscriptions. We need a listener (as discussed in c4cfceb2f2fb) that will create a Subscription whenever an auth.Profile is created. This is the beginning of that effort. It hasn't been tested, and all the pieces aren't in place, but it's a rough skeleton. We have a Dockerfile that will correctly build a minimal container for the listener. We have a build-docker.sh script that will correctly build a binary that will be used in the Dockerfile. We have a ca-certificates.crt, which are pulled from Ubuntu, and are necessary before we can safely consume SSL endpoints. We created a small consumer script that listens for events off NSQ, and calls the appropriate endpoint for our Subscriptions API. This is untested, and it doesn't build at the moment, but that's awaiting changes in the code.secondbit.org/auth.hg package. Finally, we have a wrapper.sh file that will expose the Stripe secret key being used from a Kubernetes secret file as an environment variable, instead.
| 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 } |