ducky/subscriptions
4:36e90e828dd0 Browse Files
Add an API and subscriptionsd . Create a barebones implementation of the API, including only methods to create a Subscription and retrieve the Subscription associated with a user. Also create a subscriptiond service that will bootstrap the service and stores, and get everything stood up.
api/context_helpers.go api/response.go api/subscription_handlers.go subscriptionsd/server.go
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/api/context_helpers.go Mon Jun 22 18:50:02 2015 -0400 1.3 @@ -0,0 +1,49 @@ 1.4 +package api 1.5 + 1.6 +import ( 1.7 + "errors" 1.8 + 1.9 + "code.secondbit.org/ducky/subscriptions.hg" 1.10 + 1.11 + "golang.org/x/net/context" 1.12 +) 1.13 + 1.14 +const ( 1.15 + subscriptionStoreKey = "SubscriptionStore" 1.16 + stripeKey = "Stripe" 1.17 +) 1.18 + 1.19 +var ( 1.20 + ErrSubscriptionStoreNotSet = errors.New("SubscriptionStore not set") 1.21 + ErrStripeClientNotSet = errors.New("Stripe not set") 1.22 +) 1.23 + 1.24 +func getSubscriptionStore(c context.Context) (subscriptions.SubscriptionStore, error) { 1.25 + store := c.Value(subscriptionStoreKey) 1.26 + if store == nil { 1.27 + return nil, ErrSubscriptionStoreNotSet 1.28 + } 1.29 + if s, ok := store.(subscriptions.SubscriptionStore); ok { 1.30 + return s, nil 1.31 + } 1.32 + return nil, ErrSubscriptionStoreNotSet 1.33 +} 1.34 + 1.35 +func WithSubscriptionStore(store subscriptions.SubscriptionStore, c context.Context) context.Context { 1.36 + return context.WithValue(c, subscriptionStoreKey, store) 1.37 +} 1.38 + 1.39 +func getStripeClient(c context.Context) (subscriptions.Stripe, error) { 1.40 + stripe := c.Value(stripeKey) 1.41 + if stripe == nil { 1.42 + return subscriptions.Stripe{}, ErrStripeClientNotSet 1.43 + } 1.44 + if s, ok := stripe.(subscriptions.Stripe); ok { 1.45 + return s, nil 1.46 + } 1.47 + return subscriptions.Stripe{}, ErrStripeClientNotSet 1.48 +} 1.49 + 1.50 +func WithStripeClient(stripe subscriptions.Stripe, c context.Context) context.Context { 1.51 + return context.WithValue(c, stripeKey, stripe) 1.52 +}
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 2.2 +++ b/api/response.go Mon Jun 22 18:50:02 2015 -0400 2.3 @@ -0,0 +1,11 @@ 2.4 +package api 2.5 + 2.6 +import ( 2.7 + "code.secondbit.org/api.hg" 2.8 + "code.secondbit.org/ducky/subscriptions.hg" 2.9 +) 2.10 + 2.11 +type Response struct { 2.12 + Subscriptions []subscriptions.Subscription `json:"subscriptions,omitempty"` 2.13 + Errors []api.RequestError `json:"errors,omitempty"` 2.14 +}
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/api/subscription_handlers.go Mon Jun 22 18:50:02 2015 -0400 3.3 @@ -0,0 +1,122 @@ 3.4 +package api 3.5 + 3.6 +import ( 3.7 + "net/http" 3.8 + 3.9 + "code.secondbit.org/api.hg" 3.10 + "code.secondbit.org/ducky/subscriptions.hg" 3.11 + "code.secondbit.org/trout.hg" 3.12 + "code.secondbit.org/uuid.hg" 3.13 + 3.14 + "golang.org/x/net/context" 3.15 +) 3.16 + 3.17 +const ( 3.18 + SubscriptionScope = "subscriptions" 3.19 + SubscriptionAdminScope = "subscriptions_admin" 3.20 +) 3.21 + 3.22 +func HandleSubscriptions(router *trout.Router, c context.Context) { 3.23 + router.Endpoint("/subscriptions").Methods("POST", "OPTIONS").Handler( 3.24 + api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, CreateSubscriptionHandler)))) 3.25 + router.Endpoint("/subscriptions/{id}").Methods("GET", "OPTIONS").Handler( 3.26 + api.CORSMiddleware(api.NegotiateMiddleware(api.ContextWrapper(c, GetSubscriptionHandler)))) 3.27 +} 3.28 + 3.29 +func CreateSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) { 3.30 + store, err := getSubscriptionStore(c) 3.31 + if err != nil { 3.32 + api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) 3.33 + return 3.34 + } 3.35 + stripe, err := getStripeClient(c) 3.36 + if err != nil { 3.37 + api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) 3.38 + return 3.39 + } 3.40 + if !api.CheckScopes(r, SubscriptionScope) { 3.41 + api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) 3.42 + return 3.43 + } 3.44 + userID, err := api.AuthUser(r) 3.45 + if err != nil { 3.46 + api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) 3.47 + return 3.48 + } 3.49 + var req subscriptions.SubscriptionRequest 3.50 + err = api.Decode(r, &req) 3.51 + if err != nil { 3.52 + api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError}) 3.53 + return 3.54 + } 3.55 + errs := req.Validate(userID, api.CheckScopes(r, SubscriptionAdminScope)) 3.56 + if len(errs) != 0 { 3.57 + api.Encode(w, r, http.StatusBadRequest, Response{Errors: errs}) 3.58 + return 3.59 + } 3.60 + // TODO: need some way of validating the email they sent actually belongs to them 3.61 + sub, err := subscriptions.New(req, stripe, store) 3.62 + if err != nil { 3.63 + var rErr api.RequestError 3.64 + var code int 3.65 + switch err { 3.66 + case subscriptions.ErrSubscriptionAlreadyExists: 3.67 + rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/user_id"} 3.68 + code = http.StatusBadRequest 3.69 + case subscriptions.ErrStripeSubscriptionAlreadyExists: 3.70 + rErr = api.RequestError{Slug: api.RequestErrConflict, Field: "/stripe_token"} 3.71 + code = http.StatusBadRequest 3.72 + default: 3.73 + rErr = api.RequestError{Slug: api.RequestErrActOfGod} 3.74 + code = http.StatusInternalServerError 3.75 + } 3.76 + api.Encode(w, r, code, Response{Errors: []api.RequestError{rErr}}) 3.77 + return 3.78 + } 3.79 + resp := Response{Subscriptions: []subscriptions.Subscription{sub}} 3.80 + api.Encode(w, r, http.StatusCreated, resp) 3.81 +} 3.82 + 3.83 +func GetSubscriptionHandler(w http.ResponseWriter, r *http.Request, c context.Context) { 3.84 + store, err := getSubscriptionStore(c) 3.85 + if err != nil { 3.86 + api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) 3.87 + return 3.88 + } 3.89 + vars := trout.RequestVars(r) 3.90 + rawID := vars.Get("id") 3.91 + if rawID == "" { 3.92 + api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrMissing, Param: "id"}}}) 3.93 + return 3.94 + } 3.95 + id, err := uuid.Parse(rawID) 3.96 + if err != nil { 3.97 + api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}}) 3.98 + return 3.99 + } 3.100 + if !api.CheckScopes(r, SubscriptionScope) { 3.101 + api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) 3.102 + return 3.103 + } 3.104 + userID, err := api.AuthUser(r) 3.105 + if err != nil { 3.106 + api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) 3.107 + return 3.108 + } 3.109 + if !id.Equal(userID) && !api.CheckScopes(r, SubscriptionAdminScope) { 3.110 + api.Encode(w, r, http.StatusUnauthorized, Response{Errors: []api.RequestError{{Slug: api.RequestErrAccessDenied}}}) 3.111 + return 3.112 + } 3.113 + subs, err := store.GetSubscriptions([]uuid.ID{id}) 3.114 + if err != nil { 3.115 + api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError}) 3.116 + return 3.117 + } 3.118 + sub, ok := subs[id.String()] 3.119 + if !ok { 3.120 + api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}}) 3.121 + return 3.122 + } 3.123 + resp := Response{Subscriptions: []subscriptions.Subscription{sub}} 3.124 + api.Encode(w, r, http.StatusOK, resp) 3.125 +}
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 4.2 +++ b/subscriptionsd/server.go Mon Jun 22 18:50:02 2015 -0400 4.3 @@ -0,0 +1,42 @@ 4.4 +package main 4.5 + 4.6 +import ( 4.7 + "log" 4.8 + "net/http" 4.9 + "os" 4.10 + 4.11 + "code.secondbit.org/ducky/subscriptions.hg" 4.12 + "code.secondbit.org/ducky/subscriptions.hg/api" 4.13 + "code.secondbit.org/trout.hg" 4.14 + 4.15 + "github.com/stripe/stripe-go" 4.16 + 4.17 + "golang.org/x/net/context" 4.18 +) 4.19 + 4.20 +func main() { 4.21 + log.SetFlags(log.LstdFlags | log.Llongfile) 4.22 + log.Printf("Running version '%s'\n", subscriptions.Version) 4.23 + var subscriptionStore subscriptions.SubscriptionStore 4.24 + if os.Getenv("SUBSCRIPTIONS_PG_DB") != "" { 4.25 + p, err := subscriptions.NewPostgres(os.Getenv("SUBSCRIPTIONS_PG_DB")) 4.26 + if err != nil { 4.27 + log.Fatal(err) 4.28 + } 4.29 + subscriptionStore = p 4.30 + } else { 4.31 + subscriptionStore = subscriptions.NewMemstore() 4.32 + } 4.33 + var stripeClient subscriptions.Stripe 4.34 + if os.Getenv("STRIPE_KEY") != "" { 4.35 + stripeClient = subscriptions.NewStripe(os.Getenv("STRIPE_KEY"), stripe.GetBackend(stripe.APIBackend)) 4.36 + } else { 4.37 + log.Fatal("STRIPE_KEY environment variable must be set!") 4.38 + } 4.39 + c := api.WithStripeClient(stripeClient, api.WithSubscriptionStore(subscriptionStore, context.Background())) 4.40 + router := trout.Router{} 4.41 + api.HandleSubscriptions(&router, c) 4.42 + http.Handle("/", router) 4.43 + log.Println("Listening on 9001") 4.44 + log.Fatal(http.ListenAndServe("0.0.0.0:9001", nil)) 4.45 +}