From f39ecc46bd15de291a0a95782cbc8d1278acabd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Mon, 19 Dec 2022 22:11:34 +0100 Subject: [PATCH] feat(wip): stripe integration for subscriptions --- config/config.go | 5 +- go.mod | 1 + go.sum | 4 ++ models/user.go | 1 + routes/subscription.go | 135 ++++++++++++++++++++++++++++++++++++++++ views/settings.tpl.html | 19 ++++++ 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 routes/subscription.go diff --git a/config/config.go b/config/config.go index 185a469..402349f 100644 --- a/config/config.go +++ b/config/config.go @@ -123,7 +123,10 @@ type serverConfig struct { } type subscriptionsConfig struct { - Enabled bool `yaml:"enabled" default:"false" env:"WAKAPI_SUBSCRIPTIONS_ENABLED"` + Enabled bool `yaml:"enabled" default:"false" env:"WAKAPI_SUBSCRIPTIONS_ENABLED"` + StripeApiKey string `yaml:"stripe_api_key" env:"WAKAPI_SUBSCRIPTIONS_STRIPE_API_KEY"` + StripeSecret string `yaml:"stripe_secret" env:"WAKAPI_SUBSCRIPTIONS_STRIPE_SECRET"` + StandardPriceId string `yaml:"standard_price_id" env:"WAKAPI_SUBSCRIPTIONS_STANDARD_PRICE_ID"` } type sentryConfig struct { diff --git a/go.mod b/go.mod index 9b8ee52..e142de1 100644 --- a/go.mod +++ b/go.mod @@ -67,6 +67,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect github.com/stretchr/objx v0.4.0 // indirect + github.com/stripe/stripe-go/v74 v74.3.0 // indirect github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect golang.org/x/image v0.1.0 // indirect golang.org/x/net v0.2.0 // indirect diff --git a/go.sum b/go.sum index c0fe5f2..2b63822 100644 --- a/go.sum +++ b/go.sum @@ -215,6 +215,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stripe/stripe-go/v74 v74.3.0 h1:8ymGwZvMnpWMCRNomc9dVGcJ5j8L/ubwhQvpIpcmcOA= +github.com/stripe/stripe-go/v74 v74.3.0/go.mod h1:5PoXNp30AJ3tGq57ZcFuaMylzNi8KpwlrYAFmO1fHZw= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc= @@ -262,6 +264,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -284,6 +287,7 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/models/user.go b/models/user.go index 0317935..e684f1e 100644 --- a/models/user.go +++ b/models/user.go @@ -35,6 +35,7 @@ type User struct { ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"` PublicLeaderboard bool `json:"-" gorm:"default:false; type:bool"` SubscribedUntil *CustomTime `json:"-" gorm:"type:timestamp" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"` + StripeCustomerId string `json:"-"` } type Login struct { diff --git a/routes/subscription.go b/routes/subscription.go new file mode 100644 index 0000000..69b34cc --- /dev/null +++ b/routes/subscription.go @@ -0,0 +1,135 @@ +package routes + +import ( + "fmt" + "github.com/gorilla/mux" + conf "github.com/muety/wakapi/config" + "github.com/muety/wakapi/middlewares" + "github.com/muety/wakapi/services" + "github.com/stripe/stripe-go/v74" + portalSession "github.com/stripe/stripe-go/v74/billingportal/session" + checkoutSession "github.com/stripe/stripe-go/v74/checkout/session" + "net/http" + "time" +) + +type SubscriptionHandler struct { + config *conf.Config + userSrvc services.IUserService + mailSrvc services.IMailService + httpClient *http.Client +} + +func NewSubscriptionHandler( + userService services.IUserService, + mailService services.IMailService, +) *SubscriptionHandler { + config := conf.Get() + + if config.Subscriptions.Enabled { + stripe.Key = config.Subscriptions.StripeApiKey + } + + return &SubscriptionHandler{ + config: config, + userSrvc: userService, + mailSrvc: mailService, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } +} + +// https://stripe.com/docs/billing/quickstart?lang=go + +func (h *SubscriptionHandler) RegisterRoutes(router *mux.Router) { + if !h.config.Subscriptions.Enabled { + return + } + + r := router.PathPrefix("/subscription").Subrouter() + r.Use( + middlewares.NewAuthenticateMiddleware(h.userSrvc).WithRedirectTarget(defaultErrorRedirectTarget()).Handler, + ) + r.Path("/success").Methods(http.MethodGet).HandlerFunc(h.GetCheckoutSuccess) + r.Path("/cancel").Methods(http.MethodGet).HandlerFunc(h.GetCheckoutCancel) + r.Path("/checkout").Methods(http.MethodPost).HandlerFunc(h.PostCheckout) + r.Path("/portal").Methods(http.MethodPost).HandlerFunc(h.PostPortal) + + router.Path("/webhook").Methods(http.MethodPost).HandlerFunc(h.PostWebhook) +} + +func (h *SubscriptionHandler) PostCheckout(w http.ResponseWriter, r *http.Request) { + if h.config.IsDev() { + loadTemplates() + } + + user := middlewares.GetPrincipal(r) + if user.Email == "" { + http.Redirect(w, r, fmt.Sprintf("%s/settings#subscription?error=%s", h.config.Server.BasePath, "missing e-mail address"), http.StatusFound) + return + } + + if err := r.ParseForm(); err != nil { + http.Redirect(w, r, fmt.Sprintf("%s/settings#subscription?error=%s", h.config.Server.BasePath, "missing form values"), http.StatusFound) + return + } + + checkoutParams := &stripe.CheckoutSessionParams{ + Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)), + LineItems: []*stripe.CheckoutSessionLineItemParams{ + { + Price: &h.config.Subscriptions.StandardPriceId, + Quantity: stripe.Int64(1), + }, + }, + CustomerEmail: &user.Email, + SuccessURL: stripe.String(fmt.Sprintf("%s%s/subscription/success", h.config.Server.PublicUrl, h.config.Server.BasePath)), + CancelURL: stripe.String(fmt.Sprintf("%s%s/subscription/cacnel", h.config.Server.PublicUrl, h.config.Server.BasePath)), + } + + session, err := checkoutSession.New(checkoutParams) + if err != nil { + conf.Log().Request(r).Error("failed to create stripe checkout session: %v", err) + http.Redirect(w, r, fmt.Sprintf("%s/settings#subscription?error=%s", h.config.Server.BasePath, "something went wrong"), http.StatusFound) + return + } + + http.Redirect(w, r, session.URL, http.StatusSeeOther) +} + +func (h *SubscriptionHandler) PostPortal(w http.ResponseWriter, r *http.Request) { + if h.config.IsDev() { + loadTemplates() + } + + user := middlewares.GetPrincipal(r) + if user.StripeCustomerId == "" { + http.Redirect(w, r, fmt.Sprintf("%s/settings#subscription?error=%s", h.config.Server.BasePath, "missing stripe customer reference, please contact us!"), http.StatusFound) + return + } + + portalParams := &stripe.BillingPortalSessionParams{ + Customer: &user.StripeCustomerId, + ReturnURL: &h.config.Server.PublicUrl, + } + + session, err := portalSession.New(portalParams) + if err != nil { + conf.Log().Request(r).Error("failed to create stripe portal session: %v", err) + http.Redirect(w, r, fmt.Sprintf("%s/settings#subscription?error=%s", h.config.Server.BasePath, "something went wrong"), http.StatusFound) + return + } + + http.Redirect(w, r, session.URL, http.StatusSeeOther) +} + +func (h *SubscriptionHandler) PostWebhook(w http.ResponseWriter, r *http.Request) { + // TODO: implement +} + +func (h *SubscriptionHandler) GetCheckoutSuccess(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, fmt.Sprintf("%s/settings#subscription?success=%s", h.config.Server.BasePath, "you have successfully subscribed to wakapi!"), http.StatusFound) +} + +func (h *SubscriptionHandler) GetCheckoutCancel(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, fmt.Sprintf("%s/settings#subscription", h.config.Server.BasePath), http.StatusFound) +} diff --git a/views/settings.tpl.html b/views/settings.tpl.html index a0feaf3..071afc9 100644 --- a/views/settings.tpl.html +++ b/views/settings.tpl.html @@ -49,6 +49,9 @@
  • Integrations
  • +
  • + Subscription +
  • Danger Zone
  • @@ -674,6 +677,22 @@ +
    +
    +
    +
    + Subscription + + Lorem ipsum dolor sit amet + +
    +
    + +
    +
    +
    +
    +