ducky/subscriptions

Paddy 2015-06-22 Parent:b240b6123548 Child:fe8f092cc149

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 +}