Browse Source

Merge pull request #1420 from binwiederhier/debian-stripe

WIP: Add build flags to remove Firebase, Stripe & WebPush (for Debian packaging)
Philipp C. Heckel 7 months ago
parent
commit
044326068c

+ 8 - 3
cmd/serve.go

@@ -16,10 +16,10 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/stripe/stripe-go/v74"
 	"github.com/urfave/cli/v2"
 	"github.com/urfave/cli/v2/altsrc"
 	"heckel.io/ntfy/v2/log"
+	"heckel.io/ntfy/v2/payments"
 	"heckel.io/ntfy/v2/server"
 	"heckel.io/ntfy/v2/user"
 	"heckel.io/ntfy/v2/util"
@@ -279,6 +279,8 @@ func execServe(c *cli.Context) error {
 	// Check values
 	if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
 		return errors.New("if set, FCM key file must exist")
+	} else if firebaseKeyFile != "" && !server.FirebaseAvailable {
+		return errors.New("cannot set firebase-key-file, support for Firebase is not available (nofirebase)")
 	} else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") {
 		return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys")
 	} else if keepaliveInterval < 5*time.Second {
@@ -320,6 +322,8 @@ func execServe(c *cli.Context) error {
 		return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set")
 	} else if enableSignup && !enableLogin {
 		return errors.New("cannot set enable-signup without also setting enable-login")
+	} else if !payments.Available && (stripeSecretKey != "" || stripeWebhookKey != "") {
+		return errors.New("cannot set stripe-secret-key or stripe-webhook-key, support for payments is not available in this build (nopayments)")
 	} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
 		return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
 	} else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
@@ -329,6 +333,8 @@ func execServe(c *cli.Context) error {
 		if messageSizeLimit > 5*1024*1024 {
 			return errors.New("message-size-limit cannot be higher than 5M")
 		}
+	} else if !server.WebPushAvailable && (webPushPrivateKey != "" || webPushPublicKey != "" || webPushFile != "") {
+		return errors.New("cannot enable WebPush, support is not available in this build (nowebpush)")
 	} else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration {
 		return errors.New("web push expiry warning duration cannot be higher than web push expiry duration")
 	} else if behindProxy && proxyForwardedHeader == "" {
@@ -396,8 +402,7 @@ func execServe(c *cli.Context) error {
 
 	// Stripe things
 	if stripeSecretKey != "" {
-		stripe.EnableTelemetry = false // Whoa!
-		stripe.Key = stripeSecretKey
+		payments.Setup(stripeSecretKey)
 	}
 
 	// Add default forbidden topics

+ 1 - 1
cmd/webpush.go

@@ -1,4 +1,4 @@
-//go:build !noserver
+//go:build !noserver && !nowebpush
 
 package cmd
 

+ 21 - 0
payments/payments.go

@@ -0,0 +1,21 @@
+//go:build !nopayments
+
+package payments
+
+import "github.com/stripe/stripe-go/v74"
+
+// Available is a constant used to indicate that Stripe support is available.
+// It can be disabled with the 'nopayments' build tag.
+const Available = true
+
+// SubscriptionStatus is an alias for stripe.SubscriptionStatus
+type SubscriptionStatus stripe.SubscriptionStatus
+
+// PriceRecurringInterval is an alias for stripe.PriceRecurringInterval
+type PriceRecurringInterval stripe.PriceRecurringInterval
+
+// Setup sets the Stripe secret key and disables telemetry
+func Setup(stripeSecretKey string) {
+	stripe.EnableTelemetry = false // Whoa!
+	stripe.Key = stripeSecretKey
+}

+ 18 - 0
payments/payments_dummy.go

@@ -0,0 +1,18 @@
+//go:build nopayments
+
+package payments
+
+// Available is a constant used to indicate that Stripe support is available.
+// It can be disabled with the 'nopayments' build tag.
+const Available = false
+
+// SubscriptionStatus is a dummy type
+type SubscriptionStatus string
+
+// PriceRecurringInterval is dummy type
+type PriceRecurringInterval string
+
+// Setup is a dummy type
+func Setup(stripeSecretKey string) {
+	// Nothing to see here
+}

+ 2 - 1
server/server.go

@@ -10,6 +10,7 @@ import (
 	"errors"
 	"fmt"
 	"gopkg.in/yaml.v2"
+	"heckel.io/ntfy/v2/payments"
 	"io"
 	"net"
 	"net/http"
@@ -165,7 +166,7 @@ func New(conf *Config) (*Server, error) {
 		mailer = &smtpSender{config: conf}
 	}
 	var stripe stripeAPI
-	if conf.StripeSecretKey != "" {
+	if payments.Available && conf.StripeSecretKey != "" {
 		stripe = newStripeAPI()
 	}
 	messageCache, err := createMessageCache(conf)

+ 7 - 1
server/server_firebase.go

@@ -1,3 +1,5 @@
+//go:build !nofirebase
+
 package server
 
 import (
@@ -14,6 +16,10 @@ import (
 )
 
 const (
+	// FirebaseAvailable is a constant used to indicate that Firebase support is available.
+	// It can be disabled with the 'nofirebase' build tag.
+	FirebaseAvailable = true
+
 	fcmMessageLimit         = 4000
 	fcmApnsBodyMessageLimit = 100
 )
@@ -73,7 +79,7 @@ type firebaseSenderImpl struct {
 	client *messaging.Client
 }
 
-func newFirebaseSender(credentialsFile string) (*firebaseSenderImpl, error) {
+func newFirebaseSender(credentialsFile string) (firebaseSender, error) {
 	fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile))
 	if err != nil {
 		return nil, err

+ 38 - 0
server/server_firebase_dummy.go

@@ -0,0 +1,38 @@
+//go:build nofirebase
+
+package server
+
+import (
+	"errors"
+	"heckel.io/ntfy/v2/user"
+)
+
+const (
+	// FirebaseAvailable is a constant used to indicate that Firebase support is available.
+	// It can be disabled with the 'nofirebase' build tag.
+	FirebaseAvailable = false
+)
+
+var (
+	errFirebaseNotAvailable      = errors.New("Firebase not available")
+	errFirebaseTemporarilyBanned = errors.New("visitor temporarily banned from using Firebase")
+)
+
+type firebaseClient struct {
+}
+
+func (c *firebaseClient) Send(v *visitor, m *message) error {
+	return errFirebaseNotAvailable
+}
+
+type firebaseSender interface {
+	Send(m string) error
+}
+
+func newFirebaseClient(sender firebaseSender, auther user.Auther) *firebaseClient {
+	return nil
+}
+
+func newFirebaseSender(credentialsFile string) (firebaseSender, error) {
+	return nil, errFirebaseNotAvailable
+}

+ 2 - 0
server/server_firebase_test.go

@@ -1,3 +1,5 @@
+//go:build !nofirebase
+
 package server
 
 import (

+ 6 - 3
server/server_payments.go

@@ -1,3 +1,5 @@
+//go:build !nopayments
+
 package server
 
 import (
@@ -12,6 +14,7 @@ import (
 	"github.com/stripe/stripe-go/v74/subscription"
 	"github.com/stripe/stripe-go/v74/webhook"
 	"heckel.io/ntfy/v2/log"
+	"heckel.io/ntfy/v2/payments"
 	"heckel.io/ntfy/v2/user"
 	"heckel.io/ntfy/v2/util"
 	"io"
@@ -22,7 +25,7 @@ import (
 
 // Payments in ntfy are done via Stripe.
 //
-// Pretty much all payments related things are in this file. The following processes
+// Pretty much all payments-related things are in this file. The following processes
 // handle payments:
 //
 // - Checkout:
@@ -464,8 +467,8 @@ func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, u *user.
 	billing := &user.Billing{
 		StripeCustomerID:            customerID,
 		StripeSubscriptionID:        subscriptionID,
-		StripeSubscriptionStatus:    stripe.SubscriptionStatus(status),
-		StripeSubscriptionInterval:  stripe.PriceRecurringInterval(interval),
+		StripeSubscriptionStatus:    payments.SubscriptionStatus(status),
+		StripeSubscriptionInterval:  payments.PriceRecurringInterval(interval),
 		StripeSubscriptionPaidUntil: time.Unix(paidUntil, 0),
 		StripeSubscriptionCancelAt:  time.Unix(cancelAt, 0),
 	}

+ 47 - 0
server/server_payments_dummy.go

@@ -0,0 +1,47 @@
+//go:build nopayments
+
+package server
+
+import (
+	"net/http"
+)
+
+type stripeAPI interface {
+	CancelSubscription(id string) (string, error)
+}
+
+func newStripeAPI() stripeAPI {
+	return nil
+}
+
+func (s *Server) fetchStripePrices() (map[string]int64, error) {
+	return nil, errHTTPNotFound
+}
+
+func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
+	return errHTTPNotFound
+}
+
+func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
+	return errHTTPNotFound
+}
+
+func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.ResponseWriter, r *http.Request, v *visitor) error {
+	return errHTTPNotFound
+}
+
+func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
+	return errHTTPNotFound
+}
+
+func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
+	return errHTTPNotFound
+}
+
+func (s *Server) handleAccountBillingPortalSessionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
+	return errHTTPNotFound
+}
+
+func (s *Server) handleAccountBillingWebhook(_ http.ResponseWriter, r *http.Request, v *visitor) error {
+	return errHTTPNotFound
+}

+ 16 - 13
server/server_payments_test.go

@@ -1,3 +1,5 @@
+//go:build !nopayments
+
 package server
 
 import (
@@ -6,6 +8,7 @@ import (
 	"github.com/stretchr/testify/require"
 	"github.com/stripe/stripe-go/v74"
 	"golang.org/x/time/rate"
+	"heckel.io/ntfy/v2/payments"
 	"heckel.io/ntfy/v2/user"
 	"heckel.io/ntfy/v2/util"
 	"io"
@@ -345,8 +348,8 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
 	require.Nil(t, u.Tier)
 	require.Equal(t, "", u.Billing.StripeCustomerID)
 	require.Equal(t, "", u.Billing.StripeSubscriptionID)
-	require.Equal(t, stripe.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
-	require.Equal(t, stripe.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval)
+	require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
+	require.Equal(t, payments.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval)
 	require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix())
 	require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
 	require.Equal(t, int64(0), u.Stats.Messages) // Messages and emails are not persisted for no-tier users!
@@ -362,8 +365,8 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
 	require.Equal(t, "starter", u.Tier.Code) // Not "pro"
 	require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
 	require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
-	require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus)
-	require.Equal(t, stripe.PriceRecurringIntervalMonth, u.Billing.StripeSubscriptionInterval)
+	require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus)
+	require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), u.Billing.StripeSubscriptionInterval)
 	require.Equal(t, int64(123456789), u.Billing.StripeSubscriptionPaidUntil.Unix())
 	require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
 	require.Equal(t, int64(0), u.Stats.Messages)
@@ -473,8 +476,8 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
 	billing := &user.Billing{
 		StripeCustomerID:            "acct_5555",
 		StripeSubscriptionID:        "sub_1234",
-		StripeSubscriptionStatus:    stripe.SubscriptionStatusPastDue,
-		StripeSubscriptionInterval:  stripe.PriceRecurringIntervalMonth,
+		StripeSubscriptionStatus:    payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue),
+		StripeSubscriptionInterval:  payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth),
 		StripeSubscriptionPaidUntil: time.Unix(123, 0),
 		StripeSubscriptionCancelAt:  time.Unix(456, 0),
 	}
@@ -517,10 +520,10 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
 	require.Equal(t, "starter", u.Tier.Code) // Not "pro"
 	require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
 	require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
-	require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus)     // Not "past_due"
-	require.Equal(t, stripe.PriceRecurringIntervalYear, u.Billing.StripeSubscriptionInterval) // Not "month"
-	require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix())         // Updated
-	require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix())          // Updated
+	require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus)         // Not "past_due"
+	require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalYear), u.Billing.StripeSubscriptionInterval) // Not "month"
+	require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix())                                          // Updated
+	require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix())                                           // Updated
 
 	// Verify that reservations were deleted
 	r, err := s.userManager.Reservations("phil")
@@ -580,8 +583,8 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
 	require.Nil(t, s.userManager.ChangeBilling(u.Name, &user.Billing{
 		StripeCustomerID:            "acct_5555",
 		StripeSubscriptionID:        "sub_1234",
-		StripeSubscriptionStatus:    stripe.SubscriptionStatusPastDue,
-		StripeSubscriptionInterval:  stripe.PriceRecurringIntervalMonth,
+		StripeSubscriptionStatus:    payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue),
+		StripeSubscriptionInterval:  payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth),
 		StripeSubscriptionPaidUntil: time.Unix(123, 0),
 		StripeSubscriptionCancelAt:  time.Unix(0, 0),
 	}))
@@ -598,7 +601,7 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
 	require.Nil(t, u.Tier)
 	require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
 	require.Equal(t, "", u.Billing.StripeSubscriptionID)
-	require.Equal(t, stripe.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
+	require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
 	require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix())
 	require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
 

+ 0 - 36
server/server_test.go

@@ -23,7 +23,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/SherClockHolmes/webpush-go"
 	"github.com/stretchr/testify/require"
 	"heckel.io/ntfy/v2/log"
 	"heckel.io/ntfy/v2/util"
@@ -281,30 +280,6 @@ func TestServer_WebEnabled(t *testing.T) {
 	rr = request(t, s2, "GET", "/app.html", "", nil)
 	require.Equal(t, 200, rr.Code)
 }
-
-func TestServer_WebPushEnabled(t *testing.T) {
-	conf := newTestConfig(t)
-	conf.WebRoot = "" // Disable web app
-	s := newTestServer(t, conf)
-
-	rr := request(t, s, "GET", "/manifest.webmanifest", "", nil)
-	require.Equal(t, 404, rr.Code)
-
-	conf2 := newTestConfig(t)
-	s2 := newTestServer(t, conf2)
-
-	rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil)
-	require.Equal(t, 404, rr.Code)
-
-	conf3 := newTestConfigWithWebPush(t)
-	s3 := newTestServer(t, conf3)
-
-	rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil)
-	require.Equal(t, 200, rr.Code)
-	require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type"))
-
-}
-
 func TestServer_PublishLargeMessage(t *testing.T) {
 	c := newTestConfig(t)
 	c.AttachmentCacheDir = "" // Disable attachments
@@ -3257,17 +3232,6 @@ func newTestConfigWithAuthFile(t *testing.T) *Config {
 	return conf
 }
 
-func newTestConfigWithWebPush(t *testing.T) *Config {
-	conf := newTestConfig(t)
-	privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
-	require.Nil(t, err)
-	conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db")
-	conf.WebPushEmailAddress = "testing@example.com"
-	conf.WebPushPrivateKey = privateKey
-	conf.WebPushPublicKey = publicKey
-	return conf
-}
-
 func newTestServer(t *testing.T, config *Config) *Server {
 	server, err := New(config)
 	require.Nil(t, err)

+ 6 - 0
server/server_webpush.go

@@ -1,3 +1,5 @@
+//go:build !nowebpush
+
 package server
 
 import (
@@ -13,6 +15,10 @@ import (
 )
 
 const (
+	// WebPushAvailable is a constant used to indicate that WebPush support is available.
+	// It can be disabled with the 'nowebpush' build tag.
+	WebPushAvailable = true
+
 	webPushTopicSubscribeLimit = 50
 )
 

+ 29 - 0
server/server_webpush_dummy.go

@@ -0,0 +1,29 @@
+//go:build nowebpush
+
+package server
+
+import (
+	"net/http"
+)
+
+const (
+	// WebPushAvailable is a constant used to indicate that WebPush support is available.
+	// It can be disabled with the 'nowebpush' build tag.
+	WebPushAvailable = false
+)
+
+func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
+	return errHTTPNotFound
+}
+
+func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Request, _ *visitor) error {
+	return errHTTPNotFound
+}
+
+func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
+	// Nothing to see here
+}
+
+func (s *Server) pruneAndNotifyWebPushSubscriptions() {
+	// Nothing to see here
+}

+ 37 - 0
server/server_webpush_test.go

@@ -1,8 +1,11 @@
+//go:build !nowebpush
+
 package server
 
 import (
 	"encoding/json"
 	"fmt"
+	"github.com/SherClockHolmes/webpush-go"
 	"github.com/stretchr/testify/require"
 	"heckel.io/ntfy/v2/user"
 	"heckel.io/ntfy/v2/util"
@@ -10,6 +13,7 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"net/netip"
+	"path/filepath"
 	"strings"
 	"sync/atomic"
 	"testing"
@@ -20,6 +24,28 @@ const (
 	testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF"
 )
 
+func TestServer_WebPush_Enabled(t *testing.T) {
+	conf := newTestConfig(t)
+	conf.WebRoot = "" // Disable web app
+	s := newTestServer(t, conf)
+
+	rr := request(t, s, "GET", "/manifest.webmanifest", "", nil)
+	require.Equal(t, 404, rr.Code)
+
+	conf2 := newTestConfig(t)
+	s2 := newTestServer(t, conf2)
+
+	rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil)
+	require.Equal(t, 404, rr.Code)
+
+	conf3 := newTestConfigWithWebPush(t)
+	s3 := newTestServer(t, conf3)
+
+	rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil)
+	require.Equal(t, 200, rr.Code)
+	require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type"))
+
+}
 func TestServer_WebPush_Disabled(t *testing.T) {
 	s := newTestServer(t, newTestConfig(t))
 
@@ -254,3 +280,14 @@ func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLen
 	require.Nil(t, err)
 	require.Len(t, subs, expectedLength)
 }
+
+func newTestConfigWithWebPush(t *testing.T) *Config {
+	conf := newTestConfig(t)
+	privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
+	require.Nil(t, err)
+	conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db")
+	conf.WebPushEmailAddress = "testing@example.com"
+	conf.WebPushPrivateKey = privateKey
+	conf.WebPushPublicKey = publicKey
+	return conf
+}

+ 7 - 7
user/manager.go

@@ -7,9 +7,9 @@ import (
 	"errors"
 	"fmt"
 	"github.com/mattn/go-sqlite3"
-	"github.com/stripe/stripe-go/v74"
 	"golang.org/x/crypto/bcrypt"
 	"heckel.io/ntfy/v2/log"
+	"heckel.io/ntfy/v2/payments"
 	"heckel.io/ntfy/v2/util"
 	"net/netip"
 	"path/filepath"
@@ -1242,12 +1242,12 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
 			Calls:    calls,
 		},
 		Billing: &Billing{
-			StripeCustomerID:            stripeCustomerID.String,                                          // May be empty
-			StripeSubscriptionID:        stripeSubscriptionID.String,                                      // May be empty
-			StripeSubscriptionStatus:    stripe.SubscriptionStatus(stripeSubscriptionStatus.String),       // May be empty
-			StripeSubscriptionInterval:  stripe.PriceRecurringInterval(stripeSubscriptionInterval.String), // May be empty
-			StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0),                  // May be zero
-			StripeSubscriptionCancelAt:  time.Unix(stripeSubscriptionCancelAt.Int64, 0),                   // May be zero
+			StripeCustomerID:            stripeCustomerID.String,                                            // May be empty
+			StripeSubscriptionID:        stripeSubscriptionID.String,                                        // May be empty
+			StripeSubscriptionStatus:    payments.SubscriptionStatus(stripeSubscriptionStatus.String),       // May be empty
+			StripeSubscriptionInterval:  payments.PriceRecurringInterval(stripeSubscriptionInterval.String), // May be empty
+			StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0),                    // May be zero
+			StripeSubscriptionCancelAt:  time.Unix(stripeSubscriptionCancelAt.Int64, 0),                     // May be zero
 		},
 		Deleted: deleted.Valid,
 	}

+ 2 - 3
user/manager_test.go

@@ -4,7 +4,6 @@ import (
 	"database/sql"
 	"fmt"
 	"github.com/stretchr/testify/require"
-	"github.com/stripe/stripe-go/v74"
 	"golang.org/x/crypto/bcrypt"
 	"heckel.io/ntfy/v2/util"
 	"net/netip"
@@ -164,8 +163,8 @@ func TestManager_AddUser_And_Query(t *testing.T) {
 	require.Nil(t, a.ChangeBilling("user", &Billing{
 		StripeCustomerID:            "acct_123",
 		StripeSubscriptionID:        "sub_123",
-		StripeSubscriptionStatus:    stripe.SubscriptionStatusActive,
-		StripeSubscriptionInterval:  stripe.PriceRecurringIntervalMonth,
+		StripeSubscriptionStatus:    "active",
+		StripeSubscriptionInterval:  "month",
 		StripeSubscriptionPaidUntil: time.Now().Add(time.Hour),
 		StripeSubscriptionCancelAt:  time.Unix(0, 0),
 	}))

+ 3 - 3
user/types.go

@@ -2,8 +2,8 @@ package user
 
 import (
 	"errors"
-	"github.com/stripe/stripe-go/v74"
 	"heckel.io/ntfy/v2/log"
+	"heckel.io/ntfy/v2/payments"
 	"net/netip"
 	"strings"
 	"time"
@@ -140,8 +140,8 @@ type Stats struct {
 type Billing struct {
 	StripeCustomerID            string
 	StripeSubscriptionID        string
-	StripeSubscriptionStatus    stripe.SubscriptionStatus
-	StripeSubscriptionInterval  stripe.PriceRecurringInterval
+	StripeSubscriptionStatus    payments.SubscriptionStatus
+	StripeSubscriptionInterval  payments.PriceRecurringInterval
 	StripeSubscriptionPaidUntil time.Time
 	StripeSubscriptionCancelAt  time.Time
 }