diff --git a/pkg/satellite/companies.go b/pkg/satellite/companies.go index 4ef899aef..9209197ea 100644 --- a/pkg/satellite/companies.go +++ b/pkg/satellite/companies.go @@ -24,6 +24,16 @@ type Companies interface { Update(ctx context.Context, company *Company) error } +// CompanyInfo holds data needed to create/update Company +type CompanyInfo struct { + Name string + Address string + Country string + City string + State string + PostalCode string +} + // Company is a database object that describes Company entity type Company struct { ID uuid.UUID diff --git a/pkg/satellite/satelliteweb/api.go b/pkg/satellite/satelliteweb/api.go index 30d53d22c..f7cc2a642 100644 --- a/pkg/satellite/satelliteweb/api.go +++ b/pkg/satellite/satelliteweb/api.go @@ -55,11 +55,12 @@ func (gw *gateway) grapqlHandler(w http.ResponseWriter, req *http.Request) { err = json.NewEncoder(w).Encode(result) if err != nil { - gw.logger.Error(err) + gw.log.Error(err.Error()) return } - gw.logger.Debug(result) + sugar := gw.log.Sugar() + sugar.Debug(result) } // getToken retrieves token from request diff --git a/pkg/satellite/satelliteweb/config.go b/pkg/satellite/satelliteweb/config.go index 842dee76a..6bdff0577 100644 --- a/pkg/satellite/satelliteweb/config.go +++ b/pkg/satellite/satelliteweb/config.go @@ -28,7 +28,7 @@ type Config struct { // Run implements Responsibility interface func (c Config) Run(ctx context.Context, server *provider.Provider) error { - sugar := zap.NewExample().Sugar() + log := zap.NewExample() // Create satellite DB dbURL, err := utils.ParseURL(c.DatabaseURL) @@ -42,9 +42,12 @@ func (c Config) Run(ctx context.Context, server *provider.Provider) error { } err = db.CreateTables() - sugar.Error(err) + if err != nil { + log.Error(err.Error()) + } service, err := satellite.NewService( + log, &satelliteauth.Hmac{Secret: []byte("my-suppa-secret-key")}, db, ) @@ -71,7 +74,7 @@ func (c Config) Run(ctx context.Context, server *provider.Provider) error { go (&gateway{ schema: schema, config: c.GatewayConfig, - logger: sugar, + log: log, }).run() return server.Run(ctx) diff --git a/pkg/satellite/satelliteweb/gateway.go b/pkg/satellite/satelliteweb/gateway.go index 0993fd21d..8fdd6e772 100644 --- a/pkg/satellite/satelliteweb/gateway.go +++ b/pkg/satellite/satelliteweb/gateway.go @@ -21,7 +21,7 @@ type GatewayConfig struct { type gateway struct { schema graphql.Schema config GatewayConfig - logger *zap.SugaredLogger + log *zap.Logger } func (gw *gateway) run() { @@ -36,7 +36,7 @@ func (gw *gateway) run() { } err := http.ListenAndServe(gw.config.Address, mux) - gw.logger.Errorf("unexpected exit of satellite gateway server: ", err) + gw.log.Error("unexpected exit of satellite gateway server: ", zap.Error(err)) } func (gw *gateway) appHandler(w http.ResponseWriter, req *http.Request) { diff --git a/pkg/satellite/satelliteweb/satelliteql/company.go b/pkg/satellite/satelliteweb/satelliteql/company.go new file mode 100644 index 000000000..bb743348c --- /dev/null +++ b/pkg/satellite/satelliteweb/satelliteql/company.go @@ -0,0 +1,95 @@ +// Copyright (C) 2018 Storj Labs, Inc. +// See LICENSE for copying information. + +package satelliteql + +import ( + "github.com/graphql-go/graphql" + + "storj.io/storj/pkg/satellite" +) + +const ( + companyType = "company" + companyInputType = "companyInput" + + fieldUserID = "userID" + fieldName = "name" + fieldAddress = "address" + fieldCountry = "country" + fieldCity = "city" + fieldState = "state" + fieldPostalCode = "postalCode" +) + +// graphqlCompany creates *graphql.Object type representation of satellite.Company +func graphqlCompany() *graphql.Object { + return graphql.NewObject(graphql.ObjectConfig{ + Name: companyType, + Fields: graphql.Fields{ + fieldID: &graphql.Field{ + Type: graphql.String, + }, + fieldUserID: &graphql.Field{ + Type: graphql.String, + }, + fieldName: &graphql.Field{ + Type: graphql.String, + }, + fieldAddress: &graphql.Field{ + Type: graphql.String, + }, + fieldCountry: &graphql.Field{ + Type: graphql.String, + }, + fieldCity: &graphql.Field{ + Type: graphql.String, + }, + fieldState: &graphql.Field{ + Type: graphql.String, + }, + fieldPostalCode: &graphql.Field{ + Type: graphql.String, + }, + }, + }) +} + +// graphqlCompanyInput creates graphql.InputObject type needed to register/update satellite.Company +func graphqlCompanyInput() *graphql.InputObject { + return graphql.NewInputObject(graphql.InputObjectConfig{ + Name: companyInputType, + Fields: graphql.InputObjectConfigFieldMap{ + fieldName: &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + fieldAddress: &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + fieldCountry: &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + fieldCity: &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + fieldState: &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + fieldPostalCode: &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + }, + }) +} + +// fromMapCompanyInfo creates satellite.CompanyInfo from input args +func fromMapCompanyInfo(args map[string]interface{}) (company satellite.CompanyInfo) { + company.Name, _ = args[fieldName].(string) + company.Address, _ = args[fieldAddress].(string) + company.Country, _ = args[fieldCountry].(string) + company.City, _ = args[fieldCity].(string) + company.State, _ = args[fieldState].(string) + company.PostalCode, _ = args[fieldPostalCode].(string) + + return +} diff --git a/pkg/satellite/satelliteweb/satelliteql/mutation.go b/pkg/satellite/satelliteweb/satelliteql/mutation.go index 450a32f1e..94936dc95 100644 --- a/pkg/satellite/satelliteweb/satelliteql/mutation.go +++ b/pkg/satellite/satelliteweb/satelliteql/mutation.go @@ -13,7 +13,9 @@ const ( // Mutation is graphql request that modifies data Mutation = "mutation" - registerMutation = "register" + createUserMutation = "createUser" + + input = "input" ) // rootMutation creates mutation for graphql populated by AccountsClient @@ -21,43 +23,28 @@ func rootMutation(service *satellite.Service, types Types) *graphql.Object { return graphql.NewObject(graphql.ObjectConfig{ Name: Mutation, Fields: graphql.Fields{ - registerMutation: &graphql.Field{ - Type: types.UserType(), + createUserMutation: &graphql.Field{ + Type: graphql.String, Args: graphql.FieldConfigArgument{ - fieldEmail: &graphql.ArgumentConfig{ - Type: graphql.NewNonNull(graphql.String), - }, - fieldPassword: &graphql.ArgumentConfig{ - Type: graphql.NewNonNull(graphql.String), - }, - fieldFirstName: &graphql.ArgumentConfig{ - Type: graphql.NewNonNull(graphql.String), - }, - fieldLastName: &graphql.ArgumentConfig{ - Type: graphql.NewNonNull(graphql.String), + input: &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(types.UserInput()), }, }, + // creates user and company from input params and returns userID if succeed Resolve: func(p graphql.ResolveParams) (interface{}, error) { - email, _ := p.Args[fieldEmail].(string) - password, _ := p.Args[fieldPassword].(string) - firstName, _ := p.Args[fieldFirstName].(string) - lastName, _ := p.Args[fieldLastName].(string) + var userInput = fromMapUserInfo(p.Args[input].(map[string]interface{})) - user, err := service.Register( + user, err := service.CreateUser( p.Context, - &satellite.User{ - Email: email, - FirstName: firstName, - LastName: lastName, - PasswordHash: []byte(password), - }, + userInput.User, + userInput.Company, ) if err != nil { - return nil, err + return "", err } - return user, nil + return user.ID.String(), nil }, }, }, diff --git a/pkg/satellite/satelliteweb/satelliteql/query.go b/pkg/satellite/satelliteweb/satelliteql/query.go index 51124fdc6..84299b5d6 100644 --- a/pkg/satellite/satelliteweb/satelliteql/query.go +++ b/pkg/satellite/satelliteweb/satelliteql/query.go @@ -24,7 +24,7 @@ func rootQuery(service *satellite.Service, types Types) *graphql.Object { Name: Query, Fields: graphql.Fields{ userQuery: &graphql.Field{ - Type: types.UserType(), + Type: types.User(), Args: graphql.FieldConfigArgument{ fieldID: &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), @@ -60,7 +60,7 @@ func rootQuery(service *satellite.Service, types Types) *graphql.Object { email, _ := p.Args[fieldEmail].(string) pass, _ := p.Args[fieldPassword].(string) - token, err := service.Login(p.Context, email, pass) + token, err := service.Token(p.Context, email, pass) if err != nil { return nil, err } diff --git a/pkg/satellite/satelliteweb/satelliteql/typecreator.go b/pkg/satellite/satelliteweb/satelliteql/typecreator.go index 9132aff63..cb158daaa 100644 --- a/pkg/satellite/satelliteweb/satelliteql/typecreator.go +++ b/pkg/satellite/satelliteweb/satelliteql/typecreator.go @@ -5,6 +5,7 @@ package satelliteql import ( "github.com/graphql-go/graphql" + "storj.io/storj/pkg/satellite" ) @@ -13,7 +14,11 @@ type Types interface { RootQuery() *graphql.Object RootMutation() *graphql.Object - UserType() *graphql.Object + User() *graphql.Object + Company() *graphql.Object + + UserInput() *graphql.InputObject + CompanyInput() *graphql.InputObject } // TypeCreator handles graphql type creation and error checking @@ -21,7 +26,11 @@ type TypeCreator struct { query *graphql.Object mutation *graphql.Object - user *graphql.Object + user *graphql.Object + company *graphql.Object + + userInput *graphql.InputObject + companyInput *graphql.InputObject } // RootQuery returns instance of query *graphql.Object @@ -36,11 +45,26 @@ func (c *TypeCreator) RootMutation() *graphql.Object { // Create create types and check for error func (c *TypeCreator) Create(service *satellite.Service) error { - c.user = graphqlUser() + c.company = graphqlCompany() + if err := c.company.Error(); err != nil { + return err + } + + c.companyInput = graphqlCompanyInput() + if err := c.companyInput.Error(); err != nil { + return err + } + + c.user = graphqlUser(service, c) if err := c.user.Error(); err != nil { return err } + c.userInput = graphqlUserInput(c) + if err := c.userInput.Error(); err != nil { + return err + } + c.query = rootQuery(service, c) if err := c.query.Error(); err != nil { return err @@ -54,7 +78,22 @@ func (c *TypeCreator) Create(service *satellite.Service) error { return nil } -// UserType returns instance of user *graphql.Object -func (c *TypeCreator) UserType() *graphql.Object { +// User returns instance of satellite.User *graphql.Object +func (c *TypeCreator) User() *graphql.Object { return c.user } + +// Company returns instance of satellite.Company *graphql.Object +func (c *TypeCreator) Company() *graphql.Object { + return c.company +} + +// UserInput returns instance of UserInput *graphql.Object +func (c *TypeCreator) UserInput() *graphql.InputObject { + return c.userInput +} + +// CompanyInput returns instance of CompanyInfo *graphql.Object +func (c *TypeCreator) CompanyInput() *graphql.InputObject { + return c.companyInput +} diff --git a/pkg/satellite/satelliteweb/satelliteql/user.go b/pkg/satellite/satelliteweb/satelliteql/user.go index 0c912a9e5..71e0bfecc 100644 --- a/pkg/satellite/satelliteweb/satelliteql/user.go +++ b/pkg/satellite/satelliteweb/satelliteql/user.go @@ -5,10 +5,13 @@ package satelliteql import ( "github.com/graphql-go/graphql" + + "storj.io/storj/pkg/satellite" ) const ( - userType = "user" + userType = "user" + userInputType = "userInput" fieldID = "id" fieldEmail = "email" @@ -18,8 +21,8 @@ const ( fieldCreatedAt = "createdAt" ) -// graphqlUser creates instance of user *graphql.Object -func graphqlUser() *graphql.Object { +// graphqlUser creates *graphql.Object type representation of satellite.User +func graphqlUser(service *satellite.Service, types Types) *graphql.Object { return graphql.NewObject(graphql.ObjectConfig{ Name: userType, Fields: graphql.Fields{ @@ -38,6 +41,59 @@ func graphqlUser() *graphql.Object { fieldCreatedAt: &graphql.Field{ Type: graphql.DateTime, }, + companyType: &graphql.Field{ + Type: types.Company(), + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + user, _ := p.Source.(satellite.User) + return service.GetCompany(p.Context, user.ID) + }, + }, }, }) } + +// graphqlUserInput creates graphql.InputObject type needed to register/update satellite.User +func graphqlUserInput(types Types) *graphql.InputObject { + return graphql.NewInputObject(graphql.InputObjectConfig{ + Name: userInputType, + Fields: graphql.InputObjectConfigFieldMap{ + fieldEmail: &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + fieldFirstName: &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + fieldLastName: &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + fieldPassword: &graphql.InputObjectFieldConfig{ + Type: graphql.String, + }, + companyType: &graphql.InputObjectFieldConfig{ + Type: types.CompanyInput(), + }, + }, + }) +} + +// UserInput encapsulates satellite.UserInfo and satellite.CompanyInfo which is used in user related queries +type UserInput struct { + User satellite.UserInfo + Company satellite.CompanyInfo +} + +// fromMapUserInfo creates UserInput from input args +func fromMapUserInfo(args map[string]interface{}) (input UserInput) { + input.User.Email, _ = args[fieldEmail].(string) + input.User.FirstName, _ = args[fieldFirstName].(string) + input.User.LastName, _ = args[fieldLastName].(string) + input.User.Password, _ = args[fieldPassword].(string) + + companyArgs, ok := args[companyType].(map[string]interface{}) + if !ok { + return + } + + input.Company = fromMapCompanyInfo(companyArgs) + return +} diff --git a/pkg/satellite/service.go b/pkg/satellite/service.go index 52a12206f..b0a6d6a61 100644 --- a/pkg/satellite/service.go +++ b/pkg/satellite/service.go @@ -9,6 +9,8 @@ import ( "crypto/subtle" "time" + "go.uber.org/zap" + "github.com/skyrings/skyring-common/tools/uuid" "github.com/zeebo/errs" @@ -21,10 +23,11 @@ type Service struct { Signer store DB + log *zap.Logger } // NewService returns new instance of Service -func NewService(signer Signer, store DB) (*Service, error) { +func NewService(log *zap.Logger, signer Signer, store DB) (*Service, error) { if signer == nil { return nil, errs.New("signer can't be nil") } @@ -33,24 +36,65 @@ func NewService(signer Signer, store DB) (*Service, error) { return nil, errs.New("store can't be nil") } - return &Service{Signer: signer, store: store}, nil + if log == nil { + return nil, errs.New("log can't be nil") + } + + return &Service{Signer: signer, store: store, log: log}, nil } -// Register gets password hash value and creates new user -func (s *Service) Register(ctx context.Context, user *User) (*User, error) { - passwordHash := sha256.Sum256(user.PasswordHash) - user.PasswordHash = passwordHash[:] +// CreateUser gets password hash value and creates new user +func (s *Service) CreateUser(ctx context.Context, userInfo UserInfo, companyInfo CompanyInfo) (*User, error) { + passwordHash := sha256.Sum256([]byte(userInfo.Password)) + + user, err := s.store.Users().Insert(ctx, &User{ + Email: userInfo.Email, + FirstName: userInfo.FirstName, + LastName: userInfo.LastName, + PasswordHash: passwordHash[:], + }) - newUser, err := s.store.Users().Insert(ctx, user) if err != nil { return nil, err } - return newUser, nil + _, err = s.store.Companies().Insert(ctx, &Company{ + UserID: user.ID, + Name: companyInfo.Name, + Address: companyInfo.Address, + Country: companyInfo.Country, + City: companyInfo.City, + State: companyInfo.State, + PostalCode: companyInfo.PostalCode, + }) + + if err != nil { + s.log.Error(err.Error()) + } + + return user, nil } -// Login authenticates user by credentials and returns auth token -func (s *Service) Login(ctx context.Context, email, password string) (string, error) { +// CreateCompany creates Company for authorized User +func (s *Service) CreateCompany(ctx context.Context, info CompanyInfo) (*Company, error) { + user, err := s.Authorize(ctx) + if err != nil { + return nil, err + } + + return s.store.Companies().Insert(ctx, &Company{ + UserID: user.ID, + Name: info.Name, + Address: info.Address, + Country: info.Country, + City: info.City, + State: info.State, + PostalCode: info.PostalCode, + }) +} + +// Token authenticates user by credentials and returns auth token +func (s *Service) Token(ctx context.Context, email, password string) (string, error) { passwordHash := sha256.Sum256([]byte(password)) user, err := s.store.Users().GetByCredentials(ctx, passwordHash[:], email) @@ -74,27 +118,22 @@ func (s *Service) Login(ctx context.Context, email, password string) (string, er // GetUser returns user by id func (s *Service) GetUser(ctx context.Context, id uuid.UUID) (*User, error) { - token, ok := auth.GetAPIKey(ctx) - if !ok { - return nil, errs.New("no api key was provided") - } - - claims, err := s.authenticate(string(token)) + _, err := s.Authorize(ctx) if err != nil { return nil, err } - err = s.authorize(ctx, claims) + return s.store.Users().Get(ctx, id) +} + +// GetCompany returns company by userID +func (s *Service) GetCompany(ctx context.Context, userID uuid.UUID) (*Company, error) { + _, err := s.Authorize(ctx) if err != nil { return nil, err } - user, err := s.store.Users().Get(ctx, id) - if err != nil { - return nil, err - } - - return user, nil + return s.store.Companies().GetByUserID(ctx, userID) } func (s *Service) createToken(claims *satelliteauth.Claims) (string, error) { @@ -112,6 +151,22 @@ func (s *Service) createToken(claims *satelliteauth.Claims) (string, error) { return token.String(), nil } +// Authorize validates token from context and returns authenticated and authorized User +func (s *Service) Authorize(ctx context.Context) (*User, error) { + token, ok := auth.GetAPIKey(ctx) + if !ok { + return nil, errs.New("no api key was provided") + } + + claims, err := s.authenticate(string(token)) + if err != nil { + return nil, err + } + + return s.authorize(ctx, claims) +} + +// authenticate validates toke signature and returns authenticated *satelliteauth.Claims func (s *Service) authenticate(tokenS string) (*satelliteauth.Claims, error) { token, err := satelliteauth.FromBase64URLString(tokenS) if err != nil { @@ -137,15 +192,16 @@ func (s *Service) authenticate(tokenS string) (*satelliteauth.Claims, error) { return claims, nil } -func (s *Service) authorize(ctx context.Context, claims *satelliteauth.Claims) error { +// authorize checks claims and returns authorized User +func (s *Service) authorize(ctx context.Context, claims *satelliteauth.Claims) (*User, error) { if !claims.Expiration.IsZero() && claims.Expiration.Before(time.Now()) { - return errs.New("token is outdated") + return nil, errs.New("token is outdated") } - _, err := s.store.Users().Get(ctx, claims.ID) + user, err := s.store.Users().Get(ctx, claims.ID) if err != nil { - return errs.New("authorization failed. no user with id: %s", claims.ID.String()) + return nil, errs.New("authorization failed. no user with id: %s", claims.ID.String()) } - return nil + return user, nil } diff --git a/pkg/satellite/users.go b/pkg/satellite/users.go index 5238d701e..0c303cb21 100644 --- a/pkg/satellite/users.go +++ b/pkg/satellite/users.go @@ -24,6 +24,14 @@ type Users interface { Update(ctx context.Context, user *User) error } +// UserInfo holds data needed to create/update User +type UserInfo struct { + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Email string `json:"email"` + Password string `json:"password"` +} + // User is a database object that describes User entity type User struct { ID uuid.UUID `json:"id"`