satellite/console: payments api (#3297)

This commit is contained in:
Yehor Butko 2019-10-17 17:42:18 +03:00 committed by GitHub
parent 22d0f89941
commit 26cc625dc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 281 additions and 55 deletions

View File

@ -370,7 +370,7 @@ func newNetwork(flags *Flags) (*Processes, error) {
host := "http://" + consoleAddress
createRegistrationTokenAddress := host + "/registrationToken/?projectsLimit=1"
consoleActivationAddress := host + "/activation/?token="
consoleAPIAddress := host + "/api/graphql/v0"
consoleAPIAddress := host + "/api/v0/graphql"
// wait for console server to start
time.Sleep(3 * time.Second)

View File

@ -40,6 +40,7 @@ import (
"storj.io/storj/satellite/nodestats"
"storj.io/storj/satellite/orders"
"storj.io/storj/satellite/overlay"
"storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/repair/irreparable"
"storj.io/storj/satellite/vouchers"
)
@ -363,11 +364,15 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, pointerDB metai
if consoleConfig.AuthTokenSecret == "" {
return nil, errs.New("Auth token secret required")
}
payments := stripecoinpayments.NewService(stripecoinpayments.Config{}, peer.DB.Customers(), peer.DB.CoinpaymentsTransactions())
peer.Console.Service, err = console.NewService(
peer.Log.Named("console:service"),
&consoleauth.Hmac{Secret: []byte(consoleConfig.AuthTokenSecret)},
peer.DB.Console(),
peer.DB.Rewards(),
payments.Accounts(),
consoleConfig.PasswordCost,
)
if err != nil {

View File

@ -0,0 +1,143 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleapi
import (
"context"
"encoding/json"
"net/http"
"strings"
"go.uber.org/zap"
monkit "gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/storj/pkg/auth"
"storj.io/storj/satellite/console"
)
var mon = monkit.Package()
// Payments is an api controller that exposes all payment related functionality
type Payments struct {
log *zap.Logger
service *console.Service
}
// NewPayments is a constructor for api payments controller.
func NewPayments(log *zap.Logger, service *console.Service) *Payments {
return &Payments{
log: log,
service: service,
}
}
// SetupAccount creates a payment account for the user.
func (p *Payments) SetupAccount(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusNotFound)
return
}
ctx = p.authorize(ctx, r)
err = p.service.Payments().SetupAccount(ctx)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
}
}
// AccountBalance returns an integer amount in cents that represents the current balance of payment account.
func (p *Payments) AccountBalance(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusNotFound)
return
}
ctx = p.authorize(ctx, r)
balance, err := p.service.Payments().AccountBalance(ctx)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
var balanceResponse struct {
Balance int64 `json:"balance"`
}
balanceResponse.Balance = balance
err = json.NewEncoder(w).Encode(balanceResponse)
if err != nil {
p.log.Error("failed to write json balance response", zap.Error(err))
}
}
// AddCreditCard is used to save new credit card and attach it to payment account.
func (p *Payments) AddCreditCard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusNotFound)
return
}
ctx = p.authorize(ctx, r)
var requestBody struct {
Token string `json:"token"`
}
decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&requestBody)
if err != nil {
p.serveJSONError(w, http.StatusBadRequest, err)
return
}
err = p.service.Payments().AddCreditCard(ctx, requestBody.Token)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
}
}
// serveJSONError writes JSON error to response output stream.
func (p *Payments) serveJSONError(w http.ResponseWriter, status int, err error) {
w.WriteHeader(status)
var response struct {
Error string `json:"error"`
}
response.Error = err.Error()
err = json.NewEncoder(w).Encode(response)
if err != nil {
p.log.Error("failed to write json error response", zap.Error(err))
}
}
// authorize checks request for authorization token, validates it and updates context with auth data.
func (p *Payments) authorize(ctx context.Context, r *http.Request) context.Context {
authHeaderValue := r.Header.Get("Authorization")
token := strings.TrimPrefix(authHeaderValue, "Bearer ")
auth, err := p.service.Authorize(auth.WithAPIKey(ctx, []byte(token)))
if err != nil {
return console.WithAuthFailure(ctx, err)
}
return console.WithAuth(ctx, auth)
}

View File

@ -25,6 +25,7 @@ import (
"storj.io/storj/satellite/console/consoleauth"
"storj.io/storj/satellite/console/consoleweb/consoleql"
"storj.io/storj/satellite/mailservice"
"storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/rewards"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
)
@ -49,11 +50,15 @@ func TestGrapqhlMutation(t *testing.T) {
log := zaptest.NewLogger(t)
paymentsConfig := stripecoinpayments.Config{}
payments := stripecoinpayments.NewService(paymentsConfig, db.Customers(), db.CoinpaymentsTransactions())
service, err := console.NewService(
log,
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
db.Console(),
db.Rewards(),
payments.Accounts(),
console.TestPasswordCost,
)
require.NoError(t, err)

View File

@ -20,6 +20,7 @@ import (
"storj.io/storj/satellite/console/consoleauth"
"storj.io/storj/satellite/console/consoleweb/consoleql"
"storj.io/storj/satellite/mailservice"
"storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
)
@ -30,11 +31,15 @@ func TestGraphqlQuery(t *testing.T) {
log := zaptest.NewLogger(t)
paymentsConfig := stripecoinpayments.Config{}
payments := stripecoinpayments.NewService(paymentsConfig, db.Customers(), db.CoinpaymentsTransactions())
service, err := console.NewService(
log,
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
db.Console(),
db.Rewards(),
payments.Accounts(),
console.TestPasswordCost,
)
require.NoError(t, err)

View File

@ -22,10 +22,11 @@ import (
"github.com/zeebo/errs"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
monkit "gopkg.in/spacemonkeygo/monkit.v2"
"gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/storj/pkg/auth"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/console/consoleweb/consoleapi"
"storj.io/storj/satellite/console/consoleweb/consoleql"
"storj.io/storj/satellite/mailservice"
)
@ -92,7 +93,7 @@ type Server struct {
}
}
// NewServer creates new instance of console server
// NewServer creates new instance of console server.
func NewServer(logger *zap.Logger, config Config, service *console.Service, mailService *mailservice.Service, listener net.Listener) *Server {
server := Server{
log: logger,
@ -112,25 +113,30 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
server.config.ExternalAddress = "http://" + server.listener.Addr().String() + "/"
}
mux := http.NewServeMux()
router := http.NewServeMux()
fs := http.FileServer(http.Dir(server.config.StaticDir))
mux.Handle("/api/graphql/v0", http.HandlerFunc(server.grapqlHandler))
mux.Handle("/api/v0/token", http.HandlerFunc(server.tokenHandler))
paymentController := consoleapi.NewPayments(logger, service)
router.Handle("/api/v0/payments/cards", http.HandlerFunc(paymentController.AddCreditCard))
router.Handle("/api/v0/payments/account/balance", http.HandlerFunc(paymentController.AccountBalance))
router.Handle("/api/v0/payments/account", http.HandlerFunc(paymentController.SetupAccount))
router.Handle("/api/v0/graphql", http.HandlerFunc(server.grapqlHandler))
router.Handle("/api/v0/token", http.HandlerFunc(server.tokenHandler))
router.Handle("/registrationToken/", http.HandlerFunc(server.createRegistrationTokenHandler))
router.Handle("/robots.txt", http.HandlerFunc(server.seoHandler))
if server.config.StaticDir != "" {
mux.Handle("/activation/", http.HandlerFunc(server.accountActivationHandler))
mux.Handle("/password-recovery/", http.HandlerFunc(server.passwordRecoveryHandler))
mux.Handle("/cancel-password-recovery/", http.HandlerFunc(server.cancelPasswordRecoveryHandler))
mux.Handle("/registrationToken/", http.HandlerFunc(server.createRegistrationTokenHandler))
mux.Handle("/usage-report/", http.HandlerFunc(server.bucketUsageReportHandler))
mux.Handle("/static/", server.gzipHandler(http.StripPrefix("/static", fs)))
mux.Handle("/robots.txt", http.HandlerFunc(server.seoHandler))
mux.Handle("/", http.HandlerFunc(server.appHandler))
router.Handle("/activation/", http.HandlerFunc(server.accountActivationHandler))
router.Handle("/password-recovery/", http.HandlerFunc(server.passwordRecoveryHandler))
router.Handle("/cancel-password-recovery/", http.HandlerFunc(server.cancelPasswordRecoveryHandler))
router.Handle("/usage-report/", http.HandlerFunc(server.bucketUsageReportHandler))
router.Handle("/static/", server.gzipHandler(http.StripPrefix("/static", fs)))
router.Handle("/", http.HandlerFunc(server.appHandler))
}
server.server = http.Server{
Handler: mux,
Handler: router,
MaxHeaderBytes: ContentLengthLimit.Int(),
}

View File

@ -12,11 +12,12 @@ import (
"github.com/zeebo/errs"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"gopkg.in/spacemonkeygo/monkit.v2"
monkit "gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/storj/pkg/auth"
"storj.io/storj/pkg/macaroon"
"storj.io/storj/satellite/console/consoleauth"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/rewards"
)
@ -56,21 +57,30 @@ const (
// ErrConsoleInternal describes internal console error
var ErrConsoleInternal = errs.Class("internal error")
// ErrNoMembership is error type of not belonging to a specific project
var ErrNoMembership = errs.Class("no membership error")
// Service is handling accounts related logic
//
// architecture: Service
type Service struct {
Signer
log *zap.Logger
store DB
rewards rewards.DB
log *zap.Logger
store DB
rewards rewards.DB
accounts payments.Accounts
passwordCost int
}
// PaymentsService separates all payment related functionality
type PaymentsService struct {
service *Service
}
// NewService returns new instance of Service
func NewService(log *zap.Logger, signer Signer, store DB, rewards rewards.DB, passwordCost int) (*Service, error) {
func NewService(log *zap.Logger, signer Signer, store DB, rewards rewards.DB, accounts payments.Accounts, passwordCost int) (*Service, error) {
if signer == nil {
return nil, errs.New("signer can't be nil")
}
@ -89,10 +99,52 @@ func NewService(log *zap.Logger, signer Signer, store DB, rewards rewards.DB, pa
Signer: signer,
store: store,
rewards: rewards,
accounts: accounts,
passwordCost: passwordCost,
}, nil
}
// Payments separates all payment related functionality
func (s *Service) Payments() PaymentsService {
return PaymentsService{service: s}
}
// SetupAccount creates payment account for authorized user.
func (payments PaymentsService) SetupAccount(ctx context.Context) (err error) {
defer mon.Task()(&ctx)(&err)
auth, err := GetAuth(ctx)
if err != nil {
return err
}
return payments.service.accounts.Setup(ctx, auth.User.ID, auth.User.Email)
}
// AccountBalance return account balance.
func (payments PaymentsService) AccountBalance(ctx context.Context) (balance int64, err error) {
defer mon.Task()(&ctx)(&err)
auth, err := GetAuth(ctx)
if err != nil {
return 0, err
}
return payments.service.accounts.Balance(ctx, auth.User.ID)
}
// AddCreditCard adds a card as a new payment method.
func (payments PaymentsService) AddCreditCard(ctx context.Context, creditCardToken string) (err error) {
defer mon.Task()(&ctx)(&err)
auth, err := GetAuth(ctx)
if err != nil {
return err
}
return payments.service.accounts.CreditCards().Add(ctx, auth.User.ID, creditCardToken)
}
// CreateUser gets password hash value and creates new inactive User
func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret RegistrationSecret, refUserID string) (u *User, err error) {
defer mon.Task()(&ctx)(&err)
@ -1081,9 +1133,6 @@ type isProjectMember struct {
membership *ProjectMember
}
// ErrNoMembership is error type of not belonging to a specific project
var ErrNoMembership = errs.Class("no membership error")
// isProjectOwner checks if the user is an owner of a project
func (s *Service) isProjectOwner(ctx context.Context, userID uuid.UUID, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err)

View File

@ -5,15 +5,22 @@ package payments
import (
"context"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
)
// ErrAccountNotSetup is an error type which indicates that payment account is not created.
var ErrAccountNotSetup = errs.Class("payment account is not set up")
// Accounts exposes all needed functionality to manage payment accounts.
type Accounts interface {
// Setup creates a payment account for the user.
Setup(ctx context.Context, email string) error
// If account is already set up it will return nil.
Setup(ctx context.Context, userID uuid.UUID, email string) error
// Balance returns an integer amount in cents that represents the current balance of payment account.
Balance(ctx context.Context) (int64, error)
Balance(ctx context.Context, userID uuid.UUID) (int64, error)
// CreditCards exposes all needed functionality to manage account credit cards.
CreditCards() CreditCards

View File

@ -5,21 +5,22 @@ package payments
import (
"context"
"github.com/skyrings/skyring-common/tools/uuid"
)
// CreditCards exposes all needed functionality to manage account credit cards.
type CreditCards interface {
// List returns a list of PaymentMethods for a given account.
List(ctx context.Context) ([]CreditCard, error)
List(ctx context.Context, userID uuid.UUID) ([]CreditCard, error)
// Add is used to save new credit card and attach it to payment account.
Add(ctx context.Context, cardToken string) error
Add(ctx context.Context, userID uuid.UUID, cardToken string) error
}
// CreditCard holds all public information about credit card.
type CreditCard struct {
ID []byte
ID []byte `json:"id"`
ExpMonth int `json:"exp_month"`
ExpYear int `json:"exp_year"`
Brand string `json:"brand"`

View File

@ -15,7 +15,6 @@ import (
// accounts is an implementation of payments.Accounts.
type accounts struct {
service *Service
userID uuid.UUID
}
// CreditCards exposes all needed functionality to manage account credit cards.
@ -24,8 +23,14 @@ func (accounts *accounts) CreditCards() payments.CreditCards {
}
// Setup creates a payment account for the user.
func (accounts *accounts) Setup(ctx context.Context, email string) (err error) {
defer mon.Task()(&ctx, accounts.userID, email)(&err)
// If account is already set up it will return nil.
func (accounts *accounts) Setup(ctx context.Context, userID uuid.UUID, email string) (err error) {
defer mon.Task()(&ctx, userID, email)(&err)
_, err = accounts.service.customers.GetCustomerID(ctx, userID)
if err == nil {
return nil
}
params := &stripe.CustomerParams{
Email: stripe.String(email),
@ -36,14 +41,14 @@ func (accounts *accounts) Setup(ctx context.Context, email string) (err error) {
}
// TODO: delete customer from stripe, if db insertion fails
return Error.Wrap(accounts.service.customers.Insert(ctx, accounts.userID, email))
return Error.Wrap(accounts.service.customers.Insert(ctx, userID, email))
}
// Balance returns an integer amount in cents that represents the current balance of payment account.
func (accounts *accounts) Balance(ctx context.Context) (_ int64, err error) {
defer mon.Task()(&ctx, accounts.userID)(&err)
func (accounts *accounts) Balance(ctx context.Context, userID uuid.UUID) (_ int64, err error) {
defer mon.Task()(&ctx, userID)(&err)
customerID, err := accounts.service.customers.GetCustomerID(ctx, accounts.userID)
customerID, err := accounts.service.customers.GetCustomerID(ctx, userID)
if err != nil {
return 0, Error.Wrap(err)
}

View File

@ -15,14 +15,13 @@ import (
// creditCards is an implementation of payments.CreditCards.
type creditCards struct {
service *Service
userID uuid.UUID
}
// List returns a list of PaymentMethods for a given Customer.
func (creditCards *creditCards) List(ctx context.Context) (cards []payments.CreditCard, err error) {
defer mon.Task()(&ctx, creditCards.userID)(&err)
func (creditCards *creditCards) List(ctx context.Context, userID uuid.UUID) (cards []payments.CreditCard, err error) {
defer mon.Task()(&ctx, userID)(&err)
customerID, err := creditCards.service.customers.GetCustomerID(ctx, creditCards.userID)
customerID, err := creditCards.service.customers.GetCustomerID(ctx, userID)
if err != nil {
return nil, Error.Wrap(err)
}
@ -53,12 +52,12 @@ func (creditCards *creditCards) List(ctx context.Context) (cards []payments.Cred
}
// Add is used to save new credit card and attach it to payment account.
func (creditCards *creditCards) Add(ctx context.Context, cardToken string) (err error) {
defer mon.Task()(&ctx, creditCards.userID, cardToken)(&err)
func (creditCards *creditCards) Add(ctx context.Context, userID uuid.UUID, cardToken string) (err error) {
defer mon.Task()(&ctx, userID, cardToken)(&err)
customerID, err := creditCards.service.customers.GetCustomerID(ctx, creditCards.userID)
customerID, err := creditCards.service.customers.GetCustomerID(ctx, userID)
if err != nil {
return err
return payments.ErrAccountNotSetup.Wrap(err)
}
cardParams := &stripe.PaymentMethodParams{

View File

@ -4,10 +4,9 @@
package stripecoinpayments
import (
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/stripe/stripe-go/client"
"github.com/zeebo/errs"
"gopkg.in/spacemonkeygo/monkit.v2"
monkit "gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/coinpayments"
@ -53,6 +52,6 @@ func NewService(config Config, customers CustomersDB, transactionsDB Transaction
}
// Accounts exposes all needed functionality to manage payment accounts.
func (service *Service) Accounts(userID uuid.UUID) payments.Accounts {
func (service *Service) Accounts() payments.Accounts {
return &accounts{service: service}
}

View File

@ -18,7 +18,6 @@ var _ payments.StorjTokens = (*storjTokens)(nil)
// storjTokens implements payments.StorjTokens.
type storjTokens struct {
userID uuid.UUID
service *Service
}
@ -26,10 +25,10 @@ type storjTokens struct {
// ETH wallet address where funds should be sent. There is one
// hour limit to complete the transaction. Transaction is saved to DB with
// reference to the user who made the deposit.
func (tokens *storjTokens) Deposit(ctx context.Context, amount big.Float) (_ *payments.Transaction, err error) {
defer mon.Task()(&ctx, amount)(&err)
func (tokens *storjTokens) Deposit(ctx context.Context, userID uuid.UUID, amount big.Float) (_ *payments.Transaction, err error) {
defer mon.Task()(&ctx, userID, amount)(&err)
customerID, err := tokens.service.customers.GetCustomerID(ctx, tokens.userID)
customerID, err := tokens.service.customers.GetCustomerID(ctx, userID)
if err != nil {
return nil, Error.Wrap(err)
}
@ -59,7 +58,7 @@ func (tokens *storjTokens) Deposit(ctx context.Context, amount big.Float) (_ *pa
cpTX, err := tokens.service.transactionsDB.Insert(ctx,
Transaction{
ID: tx.ID,
AccountID: tokens.userID,
AccountID: userID,
Address: tx.Address,
Amount: tx.Amount,
Received: big.Float{},
@ -73,7 +72,7 @@ func (tokens *storjTokens) Deposit(ctx context.Context, amount big.Float) (_ *pa
return &payments.Transaction{
ID: payments.TransactionID(tx.ID),
AccountID: tokens.userID,
AccountID: userID,
Amount: tx.Amount,
Received: big.Float{},
Address: tx.Address,

View File

@ -14,7 +14,7 @@ import (
// StorjTokens defines all payments STORJ token related functionality.
type StorjTokens interface {
// Deposit creates deposit transaction for specified amount.
Deposit(ctx context.Context, amount big.Float) (*Transaction, error)
Deposit(ctx context.Context, userID uuid.UUID, amount big.Float) (*Transaction, error)
}
// TransactionStatus defines allowed statuses

View File

@ -594,11 +594,14 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB, pointerDB metainfo
return nil, errs.New("Auth token secret required")
}
payments := stripecoinpayments.NewService(stripecoinpayments.Config{}, peer.DB.Customers(), peer.DB.CoinpaymentsTransactions())
peer.Console.Service, err = console.NewService(
peer.Log.Named("console:service"),
&consoleauth.Hmac{Secret: []byte(consoleConfig.AuthTokenSecret)},
peer.DB.Console(),
peer.DB.Rewards(),
payments.Accounts(),
consoleConfig.PasswordCost,
)

View File

@ -1,2 +1,2 @@
VUE_APP_STRIPE_PUBLIC_KEY=pk_test
VUE_APP_ENDPOINT_URL=/api/graphql/v0
VUE_APP_ENDPOINT_URL=/api/v0/graphql