satellite/{payments,admin}: add deletion of user creditcards on account deletion

Change-Id: I38bf7e3995846150268f7b88a70f75b0ac871b62
This commit is contained in:
stefanbenten 2020-08-19 14:43:56 +02:00 committed by Stefan Benten
parent 729079965f
commit 086a3d5348
6 changed files with 326 additions and 19 deletions

View File

@ -13,12 +13,14 @@ import (
"io/ioutil"
"math/rand"
"net/http"
"strings"
"time"
"github.com/zeebo/errs"
"storj.io/common/uuid"
"storj.io/storj/satellite/console/consoleauth"
"storj.io/storj/satellite/payments"
)
func newConsoleEndpoints(address string) *consoleEndpoints {
@ -51,6 +53,10 @@ func (ce *consoleEndpoints) SetupAccount() string {
return ce.appendPath("/api/v0/payments/account")
}
func (ce *consoleEndpoints) CreditCards() string {
return ce.appendPath("/api/v0/payments/cards")
}
func (ce *consoleEndpoints) Activation(token string) string {
return ce.appendPath("/activation/?token=" + token)
}
@ -110,6 +116,24 @@ func (ce *consoleEndpoints) createOrGetAPIKey() (string, error) {
return "", errs.Wrap(err)
}
err = ce.addCreditCard(authToken, "test")
if err != nil {
return "", errs.Wrap(err)
}
cards, err := ce.listCreditCards(authToken)
if err != nil {
return "", errs.Wrap(err)
}
if len(cards) == 0 {
return "", errs.New("no credit card(s) found")
}
err = ce.makeCreditCardDefault(authToken, cards[0].ID)
if err != nil {
return "", errs.Wrap(err)
}
projectID, err := ce.getOrCreateProject(authToken)
if err != nil {
return "", errs.Wrap(err)
@ -322,6 +346,98 @@ func (ce *consoleEndpoints) setupAccount(token string) error {
return nil
}
func (ce *consoleEndpoints) addCreditCard(token, cctoken string) error {
request, err := http.NewRequest(
http.MethodPost,
ce.CreditCards(),
strings.NewReader(cctoken))
if err != nil {
return err
}
request.AddCookie(&http.Cookie{
Name: ce.cookieName,
Value: token,
})
resp, err := ce.client.Do(request)
if err != nil {
return err
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
return nil
}
func (ce *consoleEndpoints) listCreditCards(token string) ([]payments.CreditCard, error) {
request, err := http.NewRequest(
http.MethodGet,
ce.CreditCards(),
nil)
if err != nil {
return nil, err
}
request.AddCookie(&http.Cookie{
Name: ce.cookieName,
Value: token,
})
resp, err := ce.client.Do(request)
if err != nil {
return nil, err
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return nil, errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
var list []payments.CreditCard
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&list)
if err != nil {
return nil, err
}
return list, nil
}
func (ce *consoleEndpoints) makeCreditCardDefault(token, ccID string) error {
request, err := http.NewRequest(
http.MethodPatch,
ce.CreditCards(),
strings.NewReader(ccID))
if err != nil {
return err
}
request.AddCookie(&http.Cookie{
Name: ce.cookieName,
Value: token,
})
resp, err := ce.client.Do(request)
if err != nil {
return err
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
return nil
}
func (ce *consoleEndpoints) getOrCreateProject(token string) (string, error) {
projectID, err := ce.getProject(token)
if err == nil {
@ -445,12 +561,12 @@ func generateActivationKey(userID uuid.UUID, email string, createdAt time.Time)
// TODO: change it in future, when satellite/console secret will be changed
signer := &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}
json, err := claims.JSON()
resJSON, err := claims.JSON()
if err != nil {
return "", err
}
token := consoleauth.Token{Payload: json}
token := consoleauth.Token{Payload: resJSON}
encoded := base64.URLEncoding.EncodeToString(token.Payload)
signature, err := signer.Sign([]byte(encoded))

View File

@ -370,4 +370,10 @@ func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
err.Error(), http.StatusInternalServerError)
return
}
err = server.payments.CreditCards().RemoveAll(ctx, user.ID)
if err != nil {
httpJSONError(w, "unable to delete credit card(s) from stripe account",
err.Error(), http.StatusInternalServerError)
}
}

View File

@ -22,6 +22,10 @@ type CreditCards interface {
// Remove is used to detach a credit card from payment account.
Remove(ctx context.Context, userID uuid.UUID, cardID string) error
// RemoveAll is used to detach all credit cards from payment account.
// It should only be used in case of a user deletion.
RemoveAll(ctx context.Context, userID uuid.UUID) error
// MakeDefault makes a credit card default payment method.
// this credit card should be attached to account before make it default.
MakeDefault(ctx context.Context, userID uuid.UUID, cardID string) error

View File

@ -149,3 +149,21 @@ func (creditCards *creditCards) Remove(ctx context.Context, userID uuid.UUID, ca
return Error.Wrap(err)
}
// RemoveAll is used to detach all credit cards from payment account.
// It should only be used in case of a user deletion. In case of an error, some cards could have been deleted already.
func (creditCards *creditCards) RemoveAll(ctx context.Context, userID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err)
ccList, err := creditCards.List(ctx, userID)
if err != nil {
return Error.Wrap(err)
}
for _, cc := range ccList {
_, err = creditCards.service.stripeClient.PaymentMethods().Detach(cc.ID, nil)
if err != nil {
return Error.Wrap(err)
}
}
return nil
}

View File

@ -0,0 +1,98 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
package stripecoinpayments_test
import (
"testing"
"github.com/stretchr/testify/require"
"storj.io/common/testcontext"
"storj.io/storj/private/testplanet"
)
func TestCreditCards_List(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
satellite := planet.Satellites[0]
userID := planet.Uplinks[0].Projects[0].Owner.ID
cards, err := satellite.API.Payments.Accounts.CreditCards().List(ctx, userID)
require.NoError(t, err)
require.Zero(t, cards)
})
}
func TestCreditCards_Add(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
satellite := planet.Satellites[0]
userID := planet.Uplinks[0].Projects[0].Owner.ID
err := satellite.API.Payments.Accounts.CreditCards().Add(ctx, userID, "test")
require.NoError(t, err)
cards, err := satellite.API.Payments.Accounts.CreditCards().List(ctx, userID)
require.NoError(t, err)
require.Len(t, cards, 1)
})
}
func TestCreditCards_Remove(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
satellite := planet.Satellites[0]
userID := planet.Uplinks[0].Projects[0].Owner.ID
err := satellite.API.Payments.Accounts.CreditCards().Add(ctx, userID, "test")
require.NoError(t, err)
err = satellite.API.Payments.Accounts.CreditCards().Add(ctx, userID, "test2")
require.NoError(t, err)
cards, err := satellite.API.Payments.Accounts.CreditCards().List(ctx, userID)
require.NoError(t, err)
require.Len(t, cards, 2)
// Save card that should remain after deletion.
savedCard := cards[0]
err = satellite.API.Payments.Accounts.CreditCards().Remove(ctx, userID, cards[1].ID)
require.NoError(t, err)
cards, err = satellite.API.Payments.Accounts.CreditCards().List(ctx, userID)
require.NoError(t, err)
require.Len(t, cards, 1)
require.Equal(t, savedCard, cards[0])
})
}
func TestCreditCards_RemoveAll(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
satellite := planet.Satellites[0]
userID := planet.Uplinks[0].Projects[0].Owner.ID
err := satellite.API.Payments.Accounts.CreditCards().Add(ctx, userID, "test")
require.NoError(t, err)
err = satellite.API.Payments.Accounts.CreditCards().Add(ctx, userID, "test2")
require.NoError(t, err)
cards, err := satellite.API.Payments.Accounts.CreditCards().List(ctx, userID)
require.NoError(t, err)
require.Len(t, cards, 2)
err = satellite.API.Payments.Accounts.CreditCards().RemoveAll(ctx, userID)
require.NoError(t, err)
cards, err = satellite.API.Payments.Accounts.CreditCards().List(ctx, userID)
require.NoError(t, err)
require.Len(t, cards, 0)
})
}

View File

@ -5,6 +5,7 @@ package stripecoinpayments
import (
"errors"
"fmt"
"sync"
"time"
@ -17,6 +18,7 @@ import (
"github.com/stripe/stripe-go/paymentmethod"
"storj.io/common/storj"
"storj.io/common/testrand"
"storj.io/common/uuid"
)
@ -63,7 +65,7 @@ func NewStripeMock(id storj.NodeID) StripeClient {
if !ok {
mock = &mockStripeClient{
customers: newMockCustomers(),
paymentMethods: &mockPaymentMethods{},
paymentMethods: newMockPaymentMethods(),
invoices: &mockInvoices{},
invoiceItems: &mockInvoiceItems{},
customerBalanceTransactions: newMockCustomerBalanceTransactions(),
@ -166,39 +168,102 @@ func (m *mockCustomers) Update(id string, params *stripe.CustomerParams) (*strip
}
type mockPaymentMethods struct {
// attached contains a mapping of customerID to its paymentMethods
attached map[string][]*stripe.PaymentMethod
// unattached contains created but not attached paymentMethods
unattached []*stripe.PaymentMethod
}
func newMockPaymentMethods() *mockPaymentMethods {
return &mockPaymentMethods{
attached: map[string][]*stripe.PaymentMethod{},
}
}
func (m *mockPaymentMethods) List(listParams *stripe.PaymentMethodListParams) *paymentmethod.Iter {
values := []interface{}{
&stripe.PaymentMethod{
ID: "pm_card_mastercard",
Card: &stripe.PaymentMethodCard{
ExpMonth: 12,
ExpYear: 2050,
Brand: "Mastercard",
Last4: "4444",
},
},
}
listMeta := stripe.ListMeta{
HasMore: false,
TotalCount: uint32(len(values)),
TotalCount: uint32(len(m.attached)),
}
return &paymentmethod.Iter{Iter: stripe.GetIter(nil, func(*stripe.Params, *form.Values) ([]interface{}, stripe.ListMeta, error) {
return values, listMeta, nil
mocks.Lock()
defer mocks.Unlock()
list, ok := m.attached[*listParams.Customer]
if !ok {
list = []*stripe.PaymentMethod{}
}
ret := make([]interface{}, len(list))
for i, v := range list {
ret[i] = v
}
return ret, listMeta, nil
})}
}
func (m *mockPaymentMethods) New(params *stripe.PaymentMethodParams) (*stripe.PaymentMethod, error) {
return nil, nil
randID := testrand.BucketName()
newMethod := &stripe.PaymentMethod{
ID: fmt.Sprintf("pm_card_%s", randID),
Card: &stripe.PaymentMethodCard{
ExpMonth: 12,
ExpYear: 2050,
Brand: "Mastercard",
Last4: "4444",
Description: randID,
},
Type: stripe.PaymentMethodTypeCard,
}
mocks.Lock()
defer mocks.Unlock()
m.unattached = append(m.unattached, newMethod)
return newMethod, nil
}
func (m *mockPaymentMethods) Attach(id string, params *stripe.PaymentMethodAttachParams) (*stripe.PaymentMethod, error) {
return nil, nil
var method *stripe.PaymentMethod
mocks.Lock()
defer mocks.Unlock()
for _, candidate := range m.unattached {
if candidate.ID == id {
method = candidate
}
}
attached, ok := m.attached[*params.Customer]
if !ok {
attached = []*stripe.PaymentMethod{}
}
m.attached[*params.Customer] = append(attached, method)
return method, nil
}
func (m *mockPaymentMethods) Detach(id string, params *stripe.PaymentMethodDetachParams) (*stripe.PaymentMethod, error) {
return nil, nil
var unattached *stripe.PaymentMethod
mocks.Lock()
defer mocks.Unlock()
for user, userMethods := range m.attached {
var remaining []*stripe.PaymentMethod
for _, method := range userMethods {
if id == method.ID {
unattached = method
} else {
remaining = append(remaining, method)
}
}
m.attached[user] = remaining
}
return unattached, nil
}
type mockInvoices struct {