satellite/referrals: set up referrals service and http endpoints (#3566)

This commit is contained in:
Yingrong Zhao 2019-11-25 16:36:36 -05:00 committed by GitHub
parent 17b057b33e
commit 79a4fff6c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 554 additions and 24 deletions

View File

@ -6,10 +6,11 @@ package pb
import (
context "context"
fmt "fmt"
math "math"
_ "github.com/gogo/protobuf/gogoproto"
proto "github.com/gogo/protobuf/proto"
grpc "google.golang.org/grpc"
math "math"
drpc "storj.io/drpc"
)

View File

@ -296,6 +296,7 @@ func (planet *Planet) Shutdown() error {
peer := &planet.peers[i]
errlist.Add(peer.Close())
}
for _, db := range planet.databases {
errlist.Add(db.Close())
}

View File

@ -4,6 +4,7 @@
package testplanet
import (
"context"
"os"
"path/filepath"
@ -11,8 +12,14 @@ import (
"storj.io/storj/pkg/peertls/extensions"
"storj.io/storj/pkg/peertls/tlsopts"
"storj.io/storj/pkg/server"
"storj.io/storj/private/testrand"
)
// DefaultReferralManagerServer implements the default behavior of a mock referral manager
type DefaultReferralManagerServer struct {
tokenCount int
}
// newReferralManager initializes a referral manager server
func (planet *Planet) newReferralManager() (*server.Server, error) {
prefix := "referralmanager"
@ -64,3 +71,25 @@ func (planet *Planet) newReferralManager() (*server.Server, error) {
log.Debug("id=" + identity.ID.String() + " addr=" + referralmanager.Addr().String())
return referralmanager, nil
}
// GetTokens implements a mock GetTokens endpoint that returns a number of referral tokens. By default, it returns 0 tokens.
func (server *DefaultReferralManagerServer) GetTokens(ctx context.Context, req *pb.GetTokensRequest) (*pb.GetTokensResponse, error) {
tokens := make([][]byte, server.tokenCount)
for i := 0; i < server.tokenCount; i++ {
uuid := testrand.UUID()
tokens[i] = uuid[:]
}
return &pb.GetTokensResponse{
TokenSecrets: tokens,
}, nil
}
// RedeemToken implements a mock RedeemToken endpoint.
func (server *DefaultReferralManagerServer) RedeemToken(ctx context.Context, req *pb.RedeemTokenRequest) (*pb.RedeemTokenResponse, error) {
return &pb.RedeemTokenResponse{}, nil
}
// SetTokenCount sets the number of tokens GetTokens endpoint should return.
func (server *DefaultReferralManagerServer) SetTokenCount(tokenCount int) {
server.tokenCount = tokenCount
}

View File

@ -44,6 +44,7 @@ import (
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/mockpayments"
"storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/referrals"
"storj.io/storj/satellite/repair/irreparable"
"storj.io/storj/satellite/rewards"
"storj.io/storj/satellite/vouchers"
@ -113,6 +114,10 @@ type API struct {
Version *stripecoinpayments.VersionService
}
Referrals struct {
Service *referrals.Service
}
Console struct {
Listener net.Listener
Service *console.Service
@ -437,6 +442,15 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, pointerDB metai
return nil, errs.New("Auth token secret required")
}
peer.Referrals.Service = referrals.NewService(
peer.Log.Named("referrals:service"),
signing.SignerFromFullIdentity(peer.Identity),
config.Referrals,
peer.Dialer,
peer.DB.Console().Users(),
consoleConfig.PasswordCost,
)
peer.Console.Service, err = console.NewService(
peer.Log.Named("console:service"),
&consoleauth.Hmac{Secret: []byte(consoleConfig.AuthTokenSecret)},
@ -456,6 +470,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, pointerDB metai
consoleConfig,
peer.Console.Service,
peer.Mail.Service,
peer.Referrals.Service,
peer.Console.Listener,
config.Payments.StripeCoinPayments.StripePublicKey,
)

View File

@ -89,7 +89,7 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
PartnerID string `json:"partnerId"`
Password string `json:"password"`
SecretInput string `json:"secret"`
ReferrerUserID string `json:"referrerUserID"`
ReferrerUserID string `json:"referrerUserId"`
}
err = json.NewDecoder(r.Body).Decode(&registerData)

View File

@ -0,0 +1,149 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleapi
import (
"encoding/json"
"net/http"
"github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/storj/private/post"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/console/consoleweb/consoleql"
"storj.io/storj/satellite/mailservice"
"storj.io/storj/satellite/referrals"
)
// ErrReferralsAPI - console referrals api error type.
var ErrReferralsAPI = errs.Class("console referrals api error")
// Referrals is an api controller that exposes all referrals functionality.
type Referrals struct {
log *zap.Logger
service *referrals.Service
consoleService *console.Service
mailService *mailservice.Service
ExternalAddress string
}
// NewReferrals is a constructor for api referrals controller.
func NewReferrals(log *zap.Logger, service *referrals.Service, consoleService *console.Service, mailService *mailservice.Service, externalAddress string) *Referrals {
return &Referrals{
log: log,
service: service,
consoleService: consoleService,
mailService: mailService,
ExternalAddress: externalAddress,
}
}
// GetTokens returns referral tokens based on user ID.
func (controller *Referrals) GetTokens(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
auth, err := console.GetAuth(ctx)
if err != nil {
controller.serveJSONError(w, err)
return
}
tokens, err := controller.service.GetTokens(ctx, &auth.User.ID)
if err != nil {
controller.serveJSONError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(tokens)
if err != nil {
controller.log.Error("token handler could not encode token response", zap.Error(ErrReferralsAPI.Wrap(err)))
return
}
}
// Register creates new user, sends activation e-mail.
func (controller *Referrals) Register(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
var registerData struct {
FullName string `json:"fullName"`
ShortName string `json:"shortName"`
Email string `json:"email"`
Password string `json:"password"`
ReferralToken string `json:"referralToken"`
}
err = json.NewDecoder(r.Body).Decode(&registerData)
if err != nil {
controller.serveJSONError(w, err)
return
}
user, err := controller.service.CreateUser(ctx, registerData)
if err != nil {
controller.serveJSONError(w, err)
return
}
token, err := controller.consoleService.GenerateActivationToken(ctx, user.ID, user.Email)
if err != nil {
controller.serveJSONError(w, err)
return
}
link := controller.ExternalAddress + "activation/?token=" + token
userName := user.ShortName
if user.ShortName == "" {
userName = user.FullName
}
controller.mailService.SendRenderedAsync(
ctx,
[]post.Address{{Address: user.Email, Name: userName}},
&consoleql.AccountActivationEmail{
ActivationLink: link,
Origin: controller.ExternalAddress,
},
)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(user.ID)
if err != nil {
controller.log.Error("registration handler could not encode userID", zap.Error(ErrReferralsAPI.Wrap(err)))
return
}
}
// serveJSONError writes JSON error to response output stream.
func (controller *Referrals) serveJSONError(w http.ResponseWriter, err error) {
w.WriteHeader(controller.getStatusCode(err))
var response struct {
Error string `json:"error"`
}
response.Error = err.Error()
err = json.NewEncoder(w).Encode(response)
if err != nil {
controller.log.Error("failed to write json error response", zap.Error(ErrReferralsAPI.Wrap(err)))
}
}
// getStatusCode returns http.StatusCode depends on console error class.
func (controller *Referrals) getStatusCode(err error) int {
switch {
case console.ErrValidation.Has(err):
return http.StatusBadRequest
case console.ErrUnauthorized.Has(err):
return http.StatusUnauthorized
default:
return http.StatusInternalServerError
}
}

View File

@ -31,6 +31,7 @@ import (
"storj.io/storj/satellite/console/consoleweb/consoleapi"
"storj.io/storj/satellite/console/consoleweb/consoleql"
"storj.io/storj/satellite/mailservice"
"storj.io/storj/satellite/referrals"
)
const (
@ -77,9 +78,10 @@ type Config struct {
type Server struct {
log *zap.Logger
config Config
service *console.Service
mailService *mailservice.Service
config Config
service *console.Service
mailService *mailservice.Service
referralsService *referrals.Service
listener net.Listener
server http.Server
@ -99,14 +101,15 @@ type Server struct {
}
// NewServer creates new instance of console server.
func NewServer(logger *zap.Logger, config Config, service *console.Service, mailService *mailservice.Service, listener net.Listener, stripePublicKey string) *Server {
func NewServer(logger *zap.Logger, config Config, service *console.Service, mailService *mailservice.Service, referralsService *referrals.Service, listener net.Listener, stripePublicKey string) *Server {
server := Server{
log: logger,
config: config,
listener: listener,
service: service,
mailService: mailService,
stripePublicKey: stripePublicKey,
log: logger,
config: config,
listener: listener,
service: service,
mailService: mailService,
referralsService: referralsService,
stripePublicKey: stripePublicKey,
}
logger.Sugar().Debugf("Starting Satellite UI on %s...", server.listener.Addr().String())
@ -126,6 +129,11 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
router.HandleFunc("/registrationToken/", server.createRegistrationTokenHandler)
router.HandleFunc("/robots.txt", server.seoHandler)
referralsController := consoleapi.NewReferrals(logger, referralsService, service, mailService, server.config.ExternalAddress)
referralsRouter := router.PathPrefix("/api/v0/referrals").Subrouter()
referralsRouter.Handle("/tokens", server.withAuth(http.HandlerFunc(referralsController.GetTokens))).Methods(http.MethodGet)
referralsRouter.HandleFunc("/register", referralsController.Register).Methods(http.MethodPost)
authController := consoleapi.NewAuth(logger, service, mailService, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL)
authRouter := router.PathPrefix("/api/v0/auth").Subrouter()
authRouter.Handle("/account", server.withAuth(http.HandlerFunc(authController.GetAccount))).Methods(http.MethodGet)

View File

@ -495,7 +495,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, passwor
return err
}
if err := validatePassword(password); err != nil {
if err := ValidatePassword(password); err != nil {
return err
}
@ -598,7 +598,7 @@ func (s *Service) UpdateAccount(ctx context.Context, fullName string, shortName
}
// validate fullName
err = validateFullName(fullName)
err = ValidateFullName(fullName)
if err != nil {
return ErrValidation.Wrap(err)
}
@ -635,7 +635,7 @@ func (s *Service) ChangePassword(ctx context.Context, pass, newPass string) (err
return Error.Wrap(err)
}
if err := validatePassword(newPass); err != nil {
if err := ValidatePassword(newPass); err != nil {
return err
}

View File

@ -38,7 +38,7 @@ func (user *UserInfo) IsValid() error {
var errs validationErrors
// validate fullName
if err := validateFullName(user.FullName); err != nil {
if err := ValidateFullName(user.FullName); err != nil {
errs.AddWrap(err)
}
@ -58,8 +58,8 @@ type CreateUser struct {
func (user *CreateUser) IsValid() error {
var errs validationErrors
errs.AddWrap(validateFullName(user.FullName))
errs.AddWrap(validatePassword(user.Password))
errs.AddWrap(ValidateFullName(user.FullName))
errs.AddWrap(ValidatePassword(user.Password))
// validate email
_, err := mail.ParseAddress(user.Email)

View File

@ -32,8 +32,8 @@ func (validation *validationErrors) Combine() error {
return errs.Combine(*validation...)
}
// validatePassword validates password
func validatePassword(pass string) error {
// ValidatePassword validates password
func ValidatePassword(pass string) error {
var errs validationErrors
if len(pass) < passMinLength {
@ -43,8 +43,8 @@ func validatePassword(pass string) error {
return errs.Combine()
}
// validateFullName validates full name.
func validateFullName(name string) error {
// ValidateFullName validates full name.
func ValidateFullName(name string) error {
if name == "" {
return errs.New("full name can not be empty")
}

View File

@ -3,9 +3,197 @@
package referrals
import "storj.io/storj/pkg/storj"
import (
"context"
// Config for referrals service.
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/storj/pkg/pb"
"storj.io/storj/pkg/rpc"
"storj.io/storj/pkg/signing"
"storj.io/storj/pkg/storj"
"storj.io/storj/satellite/console"
)
var mon = monkit.Package()
var (
// ErrUsedEmail is an error class for reporting already used emails.
ErrUsedEmail = errs.Class("email used error")
)
// Config is for referrals service.
type Config struct {
ReferralManagerURL storj.NodeURL
}
// Service allows communicating with the Referral Manager
//
// architecture: Service
type Service struct {
log *zap.Logger
signer signing.Signer
config Config
dialer rpc.Dialer
db console.Users
passwordCost int
}
// NewService returns a service for handling referrals information.
func NewService(log *zap.Logger, signer signing.Signer, config Config, dialer rpc.Dialer, db console.Users, passwordCost int) *Service {
return &Service{
log: log,
signer: signer,
config: config,
dialer: dialer,
db: db,
passwordCost: passwordCost,
}
}
// GetTokens returns tokens based on user ID.
func (service *Service) GetTokens(ctx context.Context, userID *uuid.UUID) (tokens []uuid.UUID, err error) {
defer mon.Task()(&ctx)(&err)
if userID.IsZero() {
return nil, errs.New("user ID is not defined")
}
conn, err := service.referralManagerConn(ctx)
if err != nil {
return nil, errs.Wrap(err)
}
defer func() {
err = conn.Close()
}()
client := conn.ReferralManagerClient()
response, err := client.GetTokens(ctx, &pb.GetTokensRequest{
OwnerUserId: userID[:],
OwnerSatelliteId: service.signer.ID(),
})
if err != nil {
return nil, errs.Wrap(err)
}
tokensInBytes := response.GetTokenSecrets()
if tokensInBytes != nil && len(tokensInBytes) == 0 {
return nil, errs.New("no available tokens")
}
tokens = make([]uuid.UUID, len(tokensInBytes))
for i := range tokensInBytes {
token, err := bytesToUUID(tokensInBytes[i])
if err != nil {
service.log.Debug("failed to convert bytes to UUID", zap.Error(err))
continue
}
tokens[i] = token
}
return tokens, nil
}
// CreateUser validates user's registration information and creates a new user.
func (service *Service) CreateUser(ctx context.Context, user CreateUser) (_ *console.User, err error) {
defer mon.Task()(&ctx)(&err)
if err := user.IsValid(); err != nil {
return nil, ErrValidation.Wrap(err)
}
if len(user.ReferralToken) == 0 {
return nil, errs.New("referral token is not defined")
}
_, err = service.db.GetByEmail(ctx, user.Email)
if err == nil {
return nil, ErrUsedEmail.New("")
}
userID, err := uuid.New()
if err != nil {
return nil, errs.Wrap(err)
}
err = service.redeemToken(ctx, userID, user.ReferralToken)
if err != nil {
return nil, errs.Wrap(err)
}
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), service.passwordCost)
if err != nil {
return nil, errs.Wrap(err)
}
newUser := &console.User{
ID: *userID,
Email: user.Email,
FullName: user.FullName,
ShortName: user.ShortName,
PasswordHash: hash,
}
u, err := service.db.Insert(ctx,
newUser,
)
if err != nil {
return nil, errs.Wrap(err)
}
return u, nil
}
func (service *Service) redeemToken(ctx context.Context, userID *uuid.UUID, token string) error {
conn, err := service.referralManagerConn(ctx)
if err != nil {
return errs.Wrap(err)
}
defer func() {
err = conn.Close()
}()
if userID.IsZero() || len(token) == 0 {
return errs.New("invalid argument")
}
referralToken, err := uuid.Parse(token)
if err != nil {
return errs.Wrap(err)
}
client := conn.ReferralManagerClient()
_, err = client.RedeemToken(ctx, &pb.RedeemTokenRequest{
Token: referralToken[:],
RedeemUserId: userID[:],
RedeemSatelliteId: service.signer.ID(),
})
if err != nil {
return errs.Wrap(err)
}
return nil
}
func (service *Service) referralManagerConn(ctx context.Context) (*rpc.Conn, error) {
if service.config.ReferralManagerURL.IsZero() {
return nil, errs.New("missing referral manager url configuration")
}
return service.dialer.DialAddressID(ctx, service.config.ReferralManagerURL.Address, service.config.ReferralManagerURL.ID)
}
// bytesToUUID is used to convert []byte to UUID
func bytesToUUID(data []byte) (uuid.UUID, error) {
var id uuid.UUID
copy(id[:], data)
if len(id) != len(data) {
return uuid.UUID{}, errs.New("Invalid uuid")
}
return id, nil
}

View File

@ -0,0 +1,93 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package referrals_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"storj.io/storj/pkg/pb"
"storj.io/storj/pkg/rpc/rpcstatus"
"storj.io/storj/private/testcontext"
"storj.io/storj/private/testplanet"
"storj.io/storj/private/testrand"
"storj.io/storj/satellite/referrals"
)
func TestServiceSuccess(t *testing.T) {
endpoint := &endpointHappyPath{}
tokenCount := 2
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
Reconfigure: testplanet.Reconfigure{
ReferralManagerServer: func(logger *zap.Logger) pb.ReferralManagerServer {
endpoint.SetTokenCount(tokenCount)
return endpoint
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
satellite := planet.Satellites[0]
userID := testrand.UUID()
tokens, err := satellite.API.Referrals.Service.GetTokens(ctx, &userID)
require.NoError(t, err)
require.Len(t, tokens, tokenCount)
user := referrals.CreateUser{
FullName: "test",
ShortName: "test",
Email: "test@mail.test",
Password: "123a123",
ReferralToken: testrand.UUID().String(),
}
createdUser, err := satellite.API.Referrals.Service.CreateUser(ctx, user)
require.NoError(t, err)
require.Equal(t, user.Email, createdUser.Email)
require.Equal(t, user.FullName, createdUser.FullName)
require.Equal(t, user.ShortName, createdUser.ShortName)
})
}
func TestServiceRedeemFailure(t *testing.T) {
endpoint := &endpointFailedPath{}
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
Reconfigure: testplanet.Reconfigure{
ReferralManagerServer: func(logger *zap.Logger) pb.ReferralManagerServer {
endpoint.SetTokenCount(2)
return endpoint
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
satellite := planet.Satellites[0]
user := referrals.CreateUser{
FullName: "test",
ShortName: "test",
Email: "test@mail.test",
Password: "123a123",
ReferralToken: testrand.UUID().String(),
}
_, err := satellite.API.Referrals.Service.CreateUser(ctx, user)
require.Error(t, err)
})
}
type endpointHappyPath struct {
testplanet.DefaultReferralManagerServer
}
type endpointFailedPath struct {
testplanet.DefaultReferralManagerServer
}
func (endpoint *endpointFailedPath) RedeemToken(ctx context.Context, req *pb.RedeemTokenRequest) (*pb.RedeemTokenResponse, error) {
return nil, rpcstatus.Error(rpcstatus.NotFound, "")
}

View File

@ -0,0 +1,46 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package referrals
import (
"net/mail"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
"storj.io/storj/satellite/console"
)
// ErrValidation validation related error class
var ErrValidation = errs.Class("validation error")
// CreateUser contains information that's necessary for creating a new user through referral program
type CreateUser struct {
FullName string `json:"fullName"`
ShortName string `json:"shortName"`
Email string `json:"email"`
Password string `json:"password"`
ReferralToken string `json:"referralToken"`
}
// IsValid checks CreateUser validity and returns error describing whats wrong.
func (user *CreateUser) IsValid() error {
var errors []error
errors = append(errors, console.ValidateFullName(user.FullName))
errors = append(errors, console.ValidatePassword(user.Password))
// validate email
_, err := mail.ParseAddress(user.Email)
errors = append(errors, err)
if user.ReferralToken != "" {
_, err := uuid.Parse(user.ReferralToken)
if err != nil {
errors = append(errors, err)
}
}
return errs.Combine(errors...)
}