satellite/{payments,admin}: add deletion of user creditcards on account deletion
Change-Id: I38bf7e3995846150268f7b88a70f75b0ac871b62
This commit is contained in:
parent
729079965f
commit
086a3d5348
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
98
satellite/payments/stripecoinpayments/creditcards_test.go
Normal file
98
satellite/payments/stripecoinpayments/creditcards_test.go
Normal 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)
|
||||
})
|
||||
}
|
@ -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",
|
||||
listMeta := stripe.ListMeta{
|
||||
HasMore: false,
|
||||
TotalCount: uint32(len(m.attached)),
|
||||
}
|
||||
return &paymentmethod.Iter{Iter: stripe.GetIter(nil, func(*stripe.Params, *form.Values) ([]interface{}, stripe.ListMeta, error) {
|
||||
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) {
|
||||
|
||||
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,
|
||||
}
|
||||
listMeta := stripe.ListMeta{
|
||||
HasMore: false,
|
||||
TotalCount: uint32(len(values)),
|
||||
}
|
||||
return &paymentmethod.Iter{Iter: stripe.GetIter(nil, func(*stripe.Params, *form.Values) ([]interface{}, stripe.ListMeta, error) {
|
||||
return values, listMeta, nil
|
||||
})}
|
||||
}
|
||||
|
||||
func (m *mockPaymentMethods) New(params *stripe.PaymentMethodParams) (*stripe.PaymentMethod, error) {
|
||||
return nil, nil
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user