Console add stripe service (#2080)

This commit is contained in:
Yaroslav Vorobiov 2019-06-03 16:46:57 +03:00 committed by GitHub
parent 8c0c518621
commit 6809129e6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 624 additions and 35 deletions

1
go.mod
View File

@ -97,6 +97,7 @@ require (
github.com/spf13/viper v1.2.1
github.com/streadway/amqp v0.0.0-20180806233856-70e15c650864 // indirect
github.com/stretchr/testify v1.3.0
github.com/stripe/stripe-go v60.17.0+incompatible
github.com/tidwall/gjson v1.1.3 // indirect
github.com/tidwall/match v0.0.0-20171002075945-1731857f09b1 // indirect
github.com/vivint/infectious v0.0.0-20190108171102-2455b059135b

2
go.sum
View File

@ -361,6 +361,8 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stripe/stripe-go v60.17.0+incompatible h1:h6fW1VPzFekkc1mVUfdmi7B4opnm153GtiNfb3bvtrQ=
github.com/stripe/stripe-go v60.17.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY=
github.com/tidwall/gjson v1.1.3 h1:u4mspaByxY+Qk4U1QYYVzGFI8qxN/3jtEV0ZDb2vRic=
github.com/tidwall/gjson v1.1.3/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA=
github.com/tidwall/match v0.0.0-20171002075945-1731857f09b1 h1:pWIN9LOlFRCJFqWIOEbHLvY0WWJddsjH2FQ6N0HKZdU=

View File

@ -23,6 +23,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/localpayments"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
)
@ -50,6 +51,7 @@ func TestGrapqhlMutation(t *testing.T) {
log,
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
db.Console(),
localpayments.NewService(nil),
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/localpayments"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
)
@ -34,6 +35,7 @@ func TestGraphqlQuery(t *testing.T) {
log,
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
db.Console(),
localpayments.NewService(nil),
console.TestPasswordCost,
)
require.NoError(t, err)

View File

@ -45,6 +45,7 @@ type Config struct {
Address string `help:"server address of the graphql api gateway and frontend app" default:"127.0.0.1:8081"`
StaticDir string `help:"path to static resources" default:""`
ExternalAddress string `help:"external endpoint of the satellite if hosted" default:""`
StripeKey string `help:"stripe api key" default:""`
// TODO: remove after Vanguard release
AuthToken string `help:"auth token needed for access to registration token creation endpoint" default:""`

View File

@ -17,6 +17,7 @@ import (
"storj.io/storj/pkg/auth"
"storj.io/storj/pkg/macaroon"
"storj.io/storj/satellite/console/consoleauth"
"storj.io/storj/satellite/payments"
)
var (
@ -57,34 +58,33 @@ const (
type Service struct {
Signer
store DB
log *zap.Logger
pm payments.Service
store DB
passwordCost int
}
// NewService returns new instance of Service
func NewService(log *zap.Logger, signer Signer, store DB, passwordCost int) (*Service, error) {
func NewService(log *zap.Logger, signer Signer, store DB, pm payments.Service, passwordCost int) (*Service, error) {
if signer == nil {
return nil, errs.New("signer can't be nil")
}
if store == nil {
return nil, errs.New("store can't be nil")
}
if log == nil {
return nil, errs.New("log can't be nil")
}
if passwordCost == 0 {
passwordCost = bcrypt.DefaultCost
}
return &Service{
log: log,
Signer: signer,
store: store,
log: log,
pm: pm,
passwordCost: passwordCost,
}, nil
}
@ -119,19 +119,35 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
return nil, errs.New(internalErrMsg)
}
u, err = s.store.Users().Insert(ctx, &User{
Email: user.Email,
FullName: user.FullName,
ShortName: user.ShortName,
PasswordHash: hash,
})
// store data
tx, err := s.store.BeginTx(ctx)
if err != nil {
return nil, errs.New(internalErrMsg)
return nil, err
}
err = s.store.RegistrationTokens().UpdateOwner(ctx, registrationToken.Secret, u.ID)
err = withTx(tx, func(tx DBTx) error {
u, err = tx.Users().Insert(ctx,
&User{
Email: user.Email,
FullName: user.FullName,
ShortName: user.ShortName,
PasswordHash: hash,
},
)
if err != nil {
return errs.New(internalErrMsg)
}
err = tx.RegistrationTokens().UpdateOwner(ctx, registrationToken.Secret, u.ID)
if err != nil {
return errs.New(internalErrMsg)
}
return nil
})
if err != nil {
return nil, errs.New(internalErrMsg)
return nil, err
}
return u, nil
@ -441,39 +457,35 @@ func (s *Service) CreateProject(ctx context.Context, projectInfo ProjectInfo) (p
return
}
project := &Project{
Description: projectInfo.Description,
Name: projectInfo.Name,
}
transaction, err := s.store.BeginTx(ctx)
tx, err := s.store.BeginTx(ctx)
if err != nil {
return nil, errs.New(internalErrMsg)
}
defer func() {
err = withTx(tx, func(tx DBTx) (err error) {
p, err = tx.Projects().Insert(ctx,
&Project{
Description: projectInfo.Description,
Name: projectInfo.Name,
},
)
if err != nil {
err = errs.Combine(err, transaction.Rollback())
return
return errs.New(internalErrMsg)
}
err = transaction.Commit()
_, err = tx.ProjectMembers().Insert(ctx, auth.User.ID, p.ID)
if err != nil {
err = errs.New(internalErrMsg)
return errs.New(internalErrMsg)
}
}()
prj, err := transaction.Projects().Insert(ctx, project)
return nil
})
if err != nil {
return nil, errs.New(internalErrMsg)
return nil, err
}
_, err = transaction.ProjectMembers().Insert(ctx, auth.User.ID, prj.ID)
if err != nil {
return nil, errs.New(internalErrMsg)
}
return prj, nil
return p, nil
}
// DeleteProject is a method for deleting project by id
@ -985,3 +997,18 @@ func (s *Service) isProjectMember(ctx context.Context, userID uuid.UUID, project
return isProjectMember{}, ErrNoMembership.New(unauthorizedErrMsg)
}
// withTx is a helper function for executing db operations
// in transaction scope
func withTx(tx DBTx, cb func(tx DBTx) error) (err error) {
defer func() {
if err != nil {
err = errs.Combine(err, tx.Rollback())
return
}
err = tx.Commit()
}()
return cb(tx)
}

View File

@ -0,0 +1,111 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package localpayments
import (
"context"
"crypto/rand"
"time"
"github.com/zeebo/errs"
"storj.io/storj/satellite/payments"
)
// storjCreationDate is a Storj creation date. TODO: correct values
var storjCreationDate = time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)
// defaultPaymentMethod represents one and only payment method for local payments,
// which attached to all customers by default
var defaultPaymentMethod = payments.PaymentMethod{
ID: []byte("0"),
CustomerID: []byte("0"),
Card: payments.Card{
Country: "us",
Brand: "visa",
Name: "Storj",
ExpMonth: 12,
ExpYear: 2022,
LastFour: "1488",
},
CreatedAt: storjCreationDate,
}
// StorjCustomer is a predefined customer
// which is linked with every user by default
var storjCustomer = payments.Customer{
ID: []byte("0"),
Name: "Storj",
Email: "storj@example.com",
CreatedAt: storjCreationDate,
}
// internalPaymentsErr is a wrapper for local payments service errors
var internalPaymentsErr = errs.Class("internal payments error")
// DB is internal payment methods storage
type DB interface {
// TODO: add method to retrieve invoice information from project invoice stamp
}
// service is internal payments.Service implementation
type service struct {
db DB
}
// NewService create new instance of local payments service
func NewService(db DB) payments.Service {
return &service{db: db}
}
// CreateCustomer creates new payments.Customer with random id to satisfy unique db constraint
func (*service) CreateCustomer(ctx context.Context, params payments.CreateCustomerParams) (*payments.Customer, error) {
var b [8]byte
_, err := rand.Read(b[:])
if err != nil {
return nil, internalPaymentsErr.New("error creating customer")
}
return &payments.Customer{
ID: b[:],
}, nil
}
// GetCustomer always returns default storjCustomer
func (*service) GetCustomer(ctx context.Context, id []byte) (*payments.Customer, error) {
return &storjCustomer, nil
}
// GetCustomerDefaultPaymentMethod always returns defaultPaymentMethod
func (*service) GetCustomerDefaultPaymentMethod(ctx context.Context, customerID []byte) (*payments.PaymentMethod, error) {
return &defaultPaymentMethod, nil
}
// GetCustomerPaymentsMethods always returns payments.Customer list with defaultPaymentMethod
func (*service) GetCustomerPaymentsMethods(ctx context.Context, customerID []byte) ([]payments.PaymentMethod, error) {
return []payments.PaymentMethod{defaultPaymentMethod}, nil
}
// GetPaymentMethod always returns defaultPaymentMethod or error
func (*service) GetPaymentMethod(ctx context.Context, id []byte) (*payments.PaymentMethod, error) {
if string(id) == "0" {
return &defaultPaymentMethod, nil
}
return nil, internalPaymentsErr.New("only one payments method exists, with id \"0\"")
}
// CreateProjectInvoice creates invoice from provided params
func (*service) CreateProjectInvoice(ctx context.Context, params payments.CreateProjectInvoiceParams) (*payments.Invoice, error) {
// TODO: fill data
return &payments.Invoice{}, nil
}
// GetInvoice retrieves invoice information from project invoice stamp by invoice id
// and returns invoice
func (*service) GetInvoice(ctx context.Context, id []byte) (*payments.Invoice, error) {
// TODO: get project invoice stamp by invoice id from the db and fill data
return &payments.Invoice{}, nil
}

View File

@ -0,0 +1,104 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package payments
import (
"context"
"time"
)
// Service is interfaces that defines behavior for working with payments
type Service interface {
CreateCustomer(ctx context.Context, params CreateCustomerParams) (*Customer, error)
GetCustomer(ctx context.Context, id []byte) (*Customer, error)
GetCustomerDefaultPaymentMethod(ctx context.Context, customerID []byte) (*PaymentMethod, error)
GetCustomerPaymentsMethods(ctx context.Context, customerID []byte) ([]PaymentMethod, error)
GetPaymentMethod(ctx context.Context, id []byte) (*PaymentMethod, error)
CreateProjectInvoice(ctx context.Context, params CreateProjectInvoiceParams) (*Invoice, error)
GetInvoice(ctx context.Context, id []byte) (*Invoice, error)
}
// CreateCustomerParams contains info needed to create new customer
type CreateCustomerParams struct {
Email string
Name string
}
// Customer contains customer info
type Customer struct {
ID []byte
Name string
Email string
CreatedAt time.Time
}
// Card contains credit card info
type Card struct {
Country string
Brand string
Name string
ExpMonth int64
ExpYear int64
LastFour string
}
// PaymentMethod contains payment method description.
// Credit cards are the only allowed payment methods so far
type PaymentMethod struct {
ID []byte
CustomerID []byte
Card Card
CreatedAt time.Time
}
// CreateProjectInvoiceParams contains info needed to create project invoice
type CreateProjectInvoiceParams struct {
ProjectName string
CustomerID string
PaymentMethodID string
Storage float64
Egress float64
ObjectCount float64
StartDate time.Time
EndDate time.Time
}
// Currency is type for allowed currency
type Currency string
const (
// CurrencyUSD is USA default currency
CurrencyUSD Currency = "usd"
)
// LineItem contains invoice line item info
type LineItem struct {
Key string
Quantity int64
Amount int64
}
// CustomField represents custom field/value field
type CustomField struct {
Name string
Value string
}
// Invoice holds invoice information
type Invoice struct {
ID []byte
PaymentMethodID []byte
Amount int64
Currency Currency
LineItems []LineItem
CustomFields []CustomField
CreatedAt time.Time
}

View File

@ -0,0 +1,324 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package stripepayments
import (
"context"
"fmt"
"time"
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/client"
"github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/storj/satellite/payments"
)
// stripeErr is a wrapper for stripe err
var stripeErr = errs.Class("stripe error")
// service is payments.Service implementation which
// works with stripe network through stripe-go client
type service struct {
log *zap.Logger
client *client.API
}
// NewService creates new instance of StripeService initialized with API key
func NewService(log *zap.Logger, apiKey string) payments.Service {
stripe.DefaultLeveledLogger = log.Sugar()
sc := new(client.API)
sc.Init(apiKey, nil)
return &service{
log: log,
client: sc,
}
}
// CreateCustomer creates new customer from CustomerParams struct
// sets default payment to one of the predefined testing VISA credit cards
func (s *service) CreateCustomer(ctx context.Context, params payments.CreateCustomerParams) (*payments.Customer, error) {
cparams := &stripe.CustomerParams{
Email: stripe.String(params.Email),
Name: stripe.String(params.Name),
}
// TODO: delete after migrating from test environment
err := cparams.SetSource("tok_visa")
if err != nil {
return nil, stripeErr.Wrap(err)
}
cus, err := s.client.Customers.New(cparams)
if err != nil {
return nil, stripeErr.Wrap(err)
}
return &payments.Customer{
ID: []byte(cus.ID),
Name: cus.Name,
Email: cus.Email,
CreatedAt: time.Unix(cus.Created, 0),
}, nil
}
// GetCustomer retrieves customer object from stripe network
func (s *service) GetCustomer(ctx context.Context, id []byte) (*payments.Customer, error) {
cus, err := s.client.Customers.Get(string(id), nil)
if err != nil {
return nil, stripeErr.Wrap(err)
}
return &payments.Customer{
ID: []byte(cus.ID),
Name: cus.Name,
Email: cus.Email,
CreatedAt: time.Unix(cus.Created, 0),
}, nil
}
// GetCustomerDefaultPaymentMethod retrieves customer default payment method from stripe network
func (s *service) GetCustomerDefaultPaymentMethod(ctx context.Context, customerID []byte) (*payments.PaymentMethod, error) {
cus, err := s.client.Customers.Get(string(customerID), nil)
if err != nil {
return nil, stripeErr.Wrap(err)
}
if cus.DefaultSource == nil {
return nil, stripeErr.New("no default payment method attached to customer")
}
pm, err := s.client.PaymentMethods.Get(cus.DefaultSource.ID, nil)
if err != nil {
return nil, stripeErr.Wrap(err)
}
if pm.Type != stripe.PaymentMethodTypeCard {
return nil, stripeErr.New("payment method other than cards are not allowed")
}
return &payments.PaymentMethod{
ID: []byte(pm.ID),
CustomerID: []byte(cus.ID),
Card: payments.Card{
Country: pm.Card.Country,
Brand: string(pm.Card.Brand),
Name: pm.BillingDetails.Name,
ExpMonth: int64(pm.Card.ExpMonth),
ExpYear: int64(pm.Card.ExpYear),
LastFour: pm.Card.Last4,
},
CreatedAt: time.Unix(pm.Created, 0),
}, nil
}
// GetCustomerPaymentsMethods retrieves all payments method attached to particular customer
func (s *service) GetCustomerPaymentsMethods(ctx context.Context, customerID []byte) ([]payments.PaymentMethod, error) {
var err error
pmparams := &stripe.PaymentMethodListParams{}
pmparams.Filters.AddFilter("customer", "", string(customerID))
pmparams.Filters.AddFilter("type", "", "card")
iterator := s.client.PaymentMethods.List(pmparams)
if err = iterator.Err(); err != nil {
return nil, stripeErr.Wrap(err)
}
var paymentMethods []payments.PaymentMethod
for iterator.Next() {
pm := iterator.PaymentMethod()
if pm.Type != stripe.PaymentMethodTypeCard {
continue
}
paymentMethods = append(paymentMethods, payments.PaymentMethod{
ID: []byte(pm.ID),
CustomerID: customerID,
Card: payments.Card{
Country: pm.Card.Country,
Brand: string(pm.Card.Brand),
Name: pm.BillingDetails.Name,
ExpMonth: int64(pm.Card.ExpMonth),
ExpYear: int64(pm.Card.ExpYear),
LastFour: pm.Card.Last4,
},
CreatedAt: time.Unix(pm.Created, 0),
})
}
return paymentMethods, nil
}
// GetPaymentMethod retrieve payment method object from stripe network
func (s *service) GetPaymentMethod(ctx context.Context, id []byte) (*payments.PaymentMethod, error) {
pm, err := s.client.PaymentMethods.Get(string(id), nil)
if err != nil {
return nil, stripeErr.Wrap(err)
}
if pm.Type != stripe.PaymentMethodTypeCard {
return nil, stripeErr.New("payment method other than cards are not allowed")
}
// TODO: check if name is always returned
var customerID []byte
if pm.Customer != nil {
customerID = []byte(pm.Customer.ID)
}
return &payments.PaymentMethod{
ID: []byte(pm.ID),
CustomerID: customerID,
Card: payments.Card{
Country: pm.Card.Country,
Brand: string(pm.Card.Brand),
Name: pm.BillingDetails.Name,
ExpMonth: int64(pm.Card.ExpMonth),
ExpYear: int64(pm.Card.ExpYear),
LastFour: pm.Card.Last4,
},
CreatedAt: time.Unix(pm.Created, 0),
}, nil
}
// CreateProjectInvoice creates new project invoice on stripe network from input params.
// Included line items:
// - Storage
// - Egress
// - ObjectsCount
// Created invoice has AutoAdvance property set to true, so it will be finalized
// (no further editing) and attempted to be paid in 1 hour after creation
func (s *service) CreateProjectInvoice(ctx context.Context, params payments.CreateProjectInvoiceParams) (*payments.Invoice, error) {
// create line items
_, err := s.client.InvoiceItems.New(&stripe.InvoiceItemParams{
Customer: stripe.String(params.CustomerID),
Description: stripe.String("Storage"),
Quantity: stripe.Int64(int64(params.Storage)),
UnitAmount: stripe.Int64(100),
Currency: stripe.String(string(stripe.CurrencyUSD)),
})
if err != nil {
return nil, stripeErr.Wrap(err)
}
_, err = s.client.InvoiceItems.New(&stripe.InvoiceItemParams{
Customer: stripe.String(params.CustomerID),
Description: stripe.String("Egress"),
Quantity: stripe.Int64(int64(params.Egress)),
UnitAmount: stripe.Int64(100),
Currency: stripe.String(string(stripe.CurrencyUSD)),
})
if err != nil {
return nil, stripeErr.Wrap(err)
}
_, err = s.client.InvoiceItems.New(&stripe.InvoiceItemParams{
Customer: stripe.String(params.CustomerID),
Description: stripe.String("ObjectsCount"),
Quantity: stripe.Int64(int64(params.ObjectCount)),
UnitAmount: stripe.Int64(100),
Currency: stripe.String(string(stripe.CurrencyUSD)),
})
if err != nil {
return nil, stripeErr.Wrap(err)
}
// TODO: fetch card info manually?
// create invoice
invoiceParams := &stripe.InvoiceParams{
Customer: stripe.String(params.CustomerID),
DefaultPaymentMethod: stripe.String(params.PaymentMethodID),
Description: stripe.String(fmt.Sprintf("Invoice for usage of %s", params.ProjectName)),
CustomFields: []*stripe.InvoiceCustomFieldParams{
{
Name: stripe.String("Billing period"),
Value: stripe.String(timeRangeString(params.StartDate, params.EndDate)),
},
{
Name: stripe.String("Project Name"),
Value: stripe.String(params.ProjectName),
},
},
AutoAdvance: stripe.Bool(true),
}
inv, err := s.client.Invoices.New(invoiceParams)
if err != nil {
return nil, stripeErr.Wrap(err)
}
// TODO: check for more items
var lineItems []payments.LineItem
for _, item := range inv.Lines.Data {
lineItems = append(lineItems, payments.LineItem{
Key: item.Description,
Quantity: item.Quantity,
Amount: item.Amount,
})
}
var customFields []payments.CustomField
for _, field := range inv.CustomFields {
customFields = append(customFields, payments.CustomField{
Name: field.Name,
Value: field.Value,
})
}
return &payments.Invoice{
ID: []byte(inv.ID),
Amount: inv.AmountDue,
Currency: payments.Currency(inv.Currency),
LineItems: lineItems,
CustomFields: customFields,
CreatedAt: time.Unix(inv.Created, 0),
}, nil
}
// GetInvoice retrieves an invoice from stripe network by invoiceID
func (s *service) GetInvoice(ctx context.Context, id []byte) (*payments.Invoice, error) {
inv, err := s.client.Invoices.Get(string(id), nil)
if err != nil {
return nil, stripeErr.Wrap(err)
}
// TODO: check for more items
var lineItems []payments.LineItem
for _, item := range inv.Lines.Data {
lineItems = append(lineItems, payments.LineItem{
Key: item.Description,
Quantity: item.Quantity,
Amount: item.Amount,
})
}
var customFields []payments.CustomField
for _, field := range inv.CustomFields {
customFields = append(customFields, payments.CustomField{
Name: field.Name,
Value: field.Value,
})
}
return &payments.Invoice{
ID: []byte(inv.ID),
Amount: inv.AmountDue,
Currency: payments.Currency(inv.Currency),
LineItems: lineItems,
CustomFields: customFields,
CreatedAt: time.Unix(inv.Created, 0),
}, nil
}
// timeRangeString helper function to create string representation of time range
func timeRangeString(start, end time.Time) string {
return fmt.Sprintf("%d/%d/%d - %d/%d/%d",
start.UTC().Month(), start.UTC().Day(), start.UTC().Year(),
end.UTC().Month(), end.UTC().Day(), end.UTC().Year())
}

View File

@ -51,6 +51,9 @@ import (
"storj.io/storj/satellite/mailservice/simulate"
"storj.io/storj/satellite/metainfo"
"storj.io/storj/satellite/orders"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/localpayments"
"storj.io/storj/satellite/payments/stripepayments"
"storj.io/storj/satellite/vouchers"
"storj.io/storj/storage"
"storj.io/storj/storage/boltdb"
@ -551,10 +554,19 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB, config *Config, ve
return nil, errs.New("Auth token secret required")
}
// TODO: change mock implementation to using mock stripe backend
var pmService payments.Service
if consoleConfig.StripeKey != "" {
pmService = stripepayments.NewService(peer.Log.Named("stripe:service"), consoleConfig.StripeKey)
} else {
pmService = localpayments.NewService(nil)
}
peer.Console.Service, err = console.NewService(
peer.Log.Named("console:service"),
&consoleauth.Hmac{Secret: []byte(consoleConfig.AuthTokenSecret)},
peer.DB.Console(),
pmService,
consoleConfig.PasswordCost,
)

View File

@ -34,6 +34,9 @@
# path to static resources
# console.static-dir: ""
# stripe api key
# console.stripe-key: ""
# satellite database connection string
# database: "postgres://"