repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
d22ca60
parent
f350303
author
Eric Bower
date
2024-03-17 15:53:52 +0000 UTC
feat(auth): stripe webhook integration (#101)

3 files changed,  +79, -0
M go.mod
M go.sum
M auth/auth.go
+75, -0
  1@@ -5,6 +5,7 @@ import (
  2 	"encoding/json"
  3 	"fmt"
  4 	"html/template"
  5+	"io"
  6 	"log/slog"
  7 	"net/http"
  8 	"net/url"
  9@@ -15,6 +16,7 @@ import (
 10 	"github.com/picosh/pico/db"
 11 	"github.com/picosh/pico/db/postgres"
 12 	"github.com/picosh/pico/shared"
 13+	stripe "github.com/stripe/stripe-go/v75"
 14 )
 15 
 16 type Client struct {
 17@@ -393,6 +395,78 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
 18 	}
 19 }
 20 
 21+func stripeWebhookHandler(w http.ResponseWriter, r *http.Request) {
 22+	client := getClient(r)
 23+	dbpool := client.Dbpool
 24+	logger := client.Logger
 25+	const MaxBodyBytes = int64(65536)
 26+	r.Body = http.MaxBytesReader(w, r.Body, MaxBodyBytes)
 27+	payload, err := io.ReadAll(r.Body)
 28+	if err != nil {
 29+		logger.Error("error reading request body", "err", err.Error())
 30+		w.WriteHeader(http.StatusServiceUnavailable)
 31+		return
 32+	}
 33+
 34+	event := stripe.Event{}
 35+
 36+	if err := json.Unmarshal(payload, &event); err != nil {
 37+		logger.Error("failed to parse webhook body JSON", "err", err.Error())
 38+		w.WriteHeader(http.StatusBadRequest)
 39+		return
 40+	}
 41+
 42+	switch event.Type {
 43+	case "checkout.session.completed":
 44+		var checkout stripe.CheckoutSession
 45+		err := json.Unmarshal(event.Data.Raw, &checkout)
 46+		if err != nil {
 47+			logger.Error("error parsing webhook JSON", "err", err.Error())
 48+			w.WriteHeader(http.StatusBadRequest)
 49+			return
 50+		}
 51+		username := checkout.ClientReferenceID
 52+		txId := ""
 53+		if checkout.PaymentIntent != nil {
 54+			txId = checkout.PaymentIntent.ID
 55+		}
 56+		email := ""
 57+		if checkout.CustomerDetails != nil {
 58+			email = checkout.CustomerDetails.Email
 59+		}
 60+		created := checkout.Created
 61+		status := checkout.PaymentStatus
 62+
 63+		log := logger.With(
 64+			"username", username,
 65+			"email", email,
 66+			"created", created,
 67+			"paymentStatus", status,
 68+			"txId", txId,
 69+		)
 70+		log.Info(
 71+			"stripe:checkout.session.completed",
 72+		)
 73+
 74+		if username == "" {
 75+			log.Error("no `?client_reference_id={username}` found in URL, cannot add pico+ membership")
 76+			w.WriteHeader(http.StatusOK)
 77+			return
 78+		}
 79+
 80+		err = dbpool.AddPicoPlusUser(username, "stripe", txId)
 81+		if err != nil {
 82+			log.Error("failed to add pico+ user", "err", err)
 83+		} else {
 84+			log.Info("successfully added pico+ user")
 85+		}
 86+	default:
 87+		// logger.Info("unhandled event type", "type", event.Type)
 88+	}
 89+
 90+	w.WriteHeader(http.StatusOK)
 91+}
 92+
 93 func createMainRoutes() []shared.Route {
 94 	fileServer := http.FileServer(http.Dir("auth/public"))
 95 
 96@@ -404,6 +478,7 @@ func createMainRoutes() []shared.Route {
 97 		shared.NewRoute("POST", "/key", keyHandler),
 98 		shared.NewRoute("GET", "/rss/(.+)", rssHandler),
 99 		shared.NewRoute("POST", "/redirect", redirectHandler),
100+		shared.NewRoute("POST", "/webhook", stripeWebhookHandler),
101 		shared.NewRoute("GET", "/main.css", fileServer.ServeHTTP),
102 		shared.NewRoute("GET", "/card.png", fileServer.ServeHTTP),
103 		shared.NewRoute("GET", "/favicon-16x16.png", fileServer.ServeHTTP),
M go.mod
+1, -0
1@@ -32,6 +32,7 @@ require (
2 	github.com/picosh/ptun v0.0.0-20240313192814-d0ca401937fe
3 	github.com/picosh/send v0.0.0-20240217194807-77b972121e63
4 	github.com/sendgrid/sendgrid-go v3.13.0+incompatible
5+	github.com/stripe/stripe-go/v75 v75.11.0
6 	github.com/yuin/goldmark v1.6.0
7 	github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
8 	github.com/yuin/goldmark-meta v1.1.0
M go.sum
+3, -0
 1@@ -243,6 +243,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 2 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 3 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 4 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 5+github.com/stripe/stripe-go/v75 v75.11.0 h1:jLbHQGRrptDS815sMKFFbTqVtrh+ugzO39zRVaU1Xe8=
 6+github.com/stripe/stripe-go/v75 v75.11.0/go.mod h1:wT44gah+eCY8Z0aSpY/vQlYYbicU9uUAbAqdaUxxDqE=
 7 github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
 8 github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k=
 9 github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
10@@ -280,6 +282,7 @@ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/
11 golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
12 golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
13 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
14+golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
15 golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
16 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
17 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=