V3-1091 Extend Users table with IsActive functionality (#1170)

* V3-1091 Extend Users table with IsActive functionality

* fixed review comments
This commit is contained in:
Yehor Butko 2019-01-30 17:04:40 +02:00 committed by GitHub
parent c5047f2364
commit 19bc01c19a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 302 additions and 60 deletions

View File

@ -18,6 +18,8 @@ const (
CreateUserMutation = "createUser"
// UpdateAccountMutation is a mutation name for account updating
UpdateAccountMutation = "updateAccount"
// ActivateAccountMutation is a mutation name for account activation
ActivateAccountMutation = "activateAccount"
// DeleteAccountMutation is a mutation name for account deletion
DeleteAccountMutation = "deleteAccount"
// ChangePasswordMutation is a mutation name for password changing
@ -97,6 +99,24 @@ func rootMutation(service *console.Service, types Types) *graphql.Object {
return auth.User, nil
},
},
ActivateAccountMutation: &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
InputArg: &graphql.ArgumentConfig{
Type: graphql.String,
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
activationToken, _ := p.Args[InputArg].(string)
token, err := service.ActivateAccount(p.Context, activationToken)
if err != nil {
return nil, err
}
return token, nil
},
},
ChangePasswordMutation: &graphql.Field{
Type: types.User(),
Args: graphql.FieldConfigArgument{

View File

@ -68,6 +68,41 @@ func TestGrapqhlMutation(t *testing.T) {
t.Fatal(err)
}
t.Run("Activate account mutation", func(t *testing.T) {
activationToken, err := service.GenerateActivationToken(
ctx,
rootUser.ID,
createUser.Email,
rootUser.CreatedAt.Add(time.Hour*24),
)
if err != nil {
t.Fatal(err)
}
query := fmt.Sprintf("mutation {activateAccount(input:\"%s\")}", activationToken)
result := graphql.Do(graphql.Params{
Schema: schema,
Context: ctx,
RequestString: query,
RootObject: make(map[string]interface{}),
})
for _, err := range result.Errors {
assert.NoError(t, err)
}
if result.HasErrors() {
t.Fatal()
}
data := result.Data.(map[string]interface{})
token := data[consoleql.ActivateAccountMutation].(string)
assert.NotEqual(t, "", token)
rootUser.Email = createUser.Email
})
token, err := service.Token(ctx, createUser.Email, createUser.Password)
if err != nil {
t.Fatal(err)
@ -122,7 +157,6 @@ func TestGrapqhlMutation(t *testing.T) {
user, err := service.GetUser(authCtx, *uID)
assert.NoError(t, err)
assert.Equal(t, newUser.Email, user.Email)
assert.Equal(t, newUser.FirstName, user.FirstName)
assert.Equal(t, newUser.LastName, user.LastName)
})
@ -336,11 +370,25 @@ func TestGrapqhlMutation(t *testing.T) {
},
Password: "123a123",
})
if err != nil {
t.Fatal(err, project)
}
activationToken1, err := service.GenerateActivationToken(
ctx,
user1.ID,
"u1@email.net",
user1.CreatedAt.Add(time.Hour*24),
)
if err != nil {
t.Fatal(err, project)
}
_, err = service.ActivateAccount(ctx, activationToken1)
if err != nil {
t.Fatal(err, project)
}
user1.Email = "u1@email.net"
user2, err := service.CreateUser(authCtx, console.CreateUser{
UserInfo: console.UserInfo{
FirstName: "User1",
@ -348,10 +396,23 @@ func TestGrapqhlMutation(t *testing.T) {
},
Password: "123a123",
})
if err != nil {
t.Fatal(err, project)
}
activationToken2, err := service.GenerateActivationToken(
ctx,
user2.ID,
"u2@email.net",
user2.CreatedAt.Add(time.Hour*24),
)
if err != nil {
t.Fatal(err, project)
}
_, err = service.ActivateAccount(ctx, activationToken2)
if err != nil {
t.Fatal(err, project)
}
user2.Email = "u2@email.net"
t.Run("Add project members mutation", func(t *testing.T) {
query := fmt.Sprintf(

View File

@ -66,6 +66,21 @@ func TestGraphqlQuery(t *testing.T) {
t.Fatal(err)
}
activationToken, err := service.GenerateActivationToken(
ctx,
rootUser.ID,
"mtest@email.com",
rootUser.CreatedAt.Add(time.Hour*24),
)
if err != nil {
t.Fatal(err)
}
_, err = service.ActivateAccount(ctx, activationToken)
if err != nil {
t.Fatal(err)
}
rootUser.Email = "mtest@email.com"
token, err := service.Token(ctx, createUser.Email, createUser.Password)
if err != nil {
t.Fatal(err)
@ -176,10 +191,23 @@ func TestGraphqlQuery(t *testing.T) {
},
Password: "123a123",
})
if err != nil {
t.Fatal(err)
}
activationToken1, err := service.GenerateActivationToken(
ctx,
user1.ID,
"muu1@email.com",
user1.CreatedAt.Add(time.Hour*24),
)
if err != nil {
t.Fatal(err)
}
_, err = service.ActivateAccount(ctx, activationToken1)
if err != nil {
t.Fatal(err)
}
user1.Email = "muu1@email.com"
user2, err := service.CreateUser(authCtx, console.CreateUser{
UserInfo: console.UserInfo{
@ -189,10 +217,23 @@ func TestGraphqlQuery(t *testing.T) {
},
Password: "123a123",
})
if err != nil {
t.Fatal(err)
}
activationToken2, err := service.GenerateActivationToken(
ctx,
user2.ID,
"muu2@email.com",
user2.CreatedAt.Add(time.Hour*24),
)
if err != nil {
t.Fatal(err)
}
_, err = service.ActivateAccount(ctx, activationToken2)
if err != nil {
t.Fatal(err)
}
user2.Email = "muu2@email.com"
err = service.AddProjectMembers(authCtx, createdProject.ID, []string{
user1.Email,

View File

@ -6,6 +6,7 @@ package console
import (
"context"
"crypto/subtle"
"fmt"
"time"
"github.com/skyrings/skyring-common/tools/uuid"
@ -24,7 +25,8 @@ var (
const (
// maxLimit specifies the limit for all paged queries
maxLimit = 50
maxLimit = 50
tokenExpirationTime = 24 * time.Hour
)
// Service is handling accounts related logic
@ -52,28 +54,99 @@ func NewService(log *zap.Logger, signer Signer, store DB) (*Service, error) {
return &Service{Signer: signer, store: store, log: log}, nil
}
// CreateUser gets password hash value and creates new User
// CreateUser gets password hash value and creates new inactive User
func (s *Service) CreateUser(ctx context.Context, user CreateUser) (u *User, err error) {
defer mon.Task()(&ctx)(&err)
if err := user.IsValid(); err != nil {
return nil, err
}
//TODO: store original email input in the db,
// TODO: store original email input in the db,
// add normalization
email := normalizeEmail(user.Email)
u, err = s.store.Users().GetByEmail(ctx, email)
if u != nil {
return nil, errs.New(fmt.Sprintf("%s is already in use", email))
}
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
return s.store.Users().Insert(ctx, &User{
Email: email,
u, err = s.store.Users().Insert(ctx, &User{
FirstName: user.FirstName,
LastName: user.LastName,
PasswordHash: hash,
})
// TODO: send "finish registration email" when email service will be ready
//activationToken, err := s.GenerateActivationToken(ctx, u.ID, email, u.CreatedAt.Add(tokenExpirationTime))
return u, err
}
// GenerateActivationToken - is a method for generating activation token
func (s *Service) GenerateActivationToken(ctx context.Context, id uuid.UUID, email string, expirationDate time.Time) (activationToken string, err error) {
defer mon.Task()(&ctx)(&err)
claims := &consoleauth.Claims{
ID: id,
Email: email,
Expiration: expirationDate,
}
return s.createToken(claims)
}
// ActivateAccount - is a method for activating user account after registration
func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (authToken string, err error) {
defer mon.Task()(&ctx)(&err)
token, err := consoleauth.FromBase64URLString(activationToken)
if err != nil {
return
}
claims, err := s.authenticate(token)
if err != nil {
return
}
user, err := s.store.Users().Get(ctx, claims.ID)
if err != nil {
return
}
now := time.Now()
if user.Email != "" {
return "", errs.New("account is already active")
}
if now.After(user.CreatedAt.Add(tokenExpirationTime)) {
return "", errs.New("activation token is expired")
}
user.Email = normalizeEmail(claims.Email)
err = s.store.Users().Update(ctx, user)
if err != nil {
return "", err
}
claims = &consoleauth.Claims{
ID: user.ID,
Expiration: time.Now().Add(tokenExpirationTime),
}
authToken, err = s.createToken(claims)
if err != nil {
return "", err
}
return authToken, err
}
// Token authenticates User by credentials and returns auth token
@ -92,10 +165,9 @@ func (s *Service) Token(ctx context.Context, email, password string) (token stri
return "", ErrUnauthorized.New("password is incorrect: %s", err.Error())
}
// TODO: move expiration time to constants
claims := consoleauth.Claims{
ID: user.ID,
Expiration: time.Now().Add(time.Minute * 15),
Expiration: time.Now().Add(tokenExpirationTime),
}
token, err = s.createToken(&claims)

View File

@ -13,26 +13,26 @@ import (
// Users exposes methods to manage User table in database.
type Users interface {
// Get is a method for querying user from the database by id.
Get(ctx context.Context, id uuid.UUID) (*User, error)
// GetByEmail is a method for querying user by email from the database.
GetByEmail(ctx context.Context, email string) (*User, error)
// Get is a method for querying user from the database by id
Get(ctx context.Context, id uuid.UUID) (*User, error)
// Insert is a method for inserting user into the database
// Insert is a method for inserting user into the database.
Insert(ctx context.Context, user *User) (*User, error)
// Delete is a method for deleting user by Id from the database.
Delete(ctx context.Context, id uuid.UUID) error
// Update is a method for updating user entity
// Update is a method for updating user entity.
Update(ctx context.Context, user *User) error
}
// UserInfo holds User updatable data
// UserInfo holds User updatable data.
type UserInfo struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
}
// IsValid checks UserInfo validity and returns error describing whats wrong
// IsValid checks UserInfo validity and returns error describing whats wrong.
func (user *UserInfo) IsValid() error {
var errs validationErrors
@ -48,13 +48,13 @@ func (user *UserInfo) IsValid() error {
return errs.Combine()
}
// CreateUser struct holds info for User creation
// CreateUser struct holds info for User creation.
type CreateUser struct {
UserInfo
Password string `json:"password"`
}
// IsValid checks CreateUser validity and returns error describing whats wrong
// IsValid checks CreateUser validity and returns error describing whats wrong.
func (user *CreateUser) IsValid() error {
var errs validationErrors
@ -64,12 +64,13 @@ func (user *CreateUser) IsValid() error {
return errs.Combine()
}
// User is a database object that describes User entity
// User is a database object that describes User entity.
type User struct {
ID uuid.UUID `json:"id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
PasswordHash []byte `json:"passwordHash"`

View File

@ -245,13 +245,14 @@ model user (
key id
unique email
field id blob
field first_name text ( updatable )
field last_name text ( updatable )
field email text ( updatable )
field password_hash blob ( updatable )
field id blob
field first_name text ( updatable )
field last_name text ( updatable )
field created_at timestamp ( autoinsert )
field email text ( updatable, nullable )
field password_hash blob ( updatable )
field created_at timestamp ( autoinsert )
)
read one (
select user

View File

@ -364,7 +364,7 @@ CREATE TABLE users (
id bytea NOT NULL,
first_name text NOT NULL,
last_name text NOT NULL,
email text NOT NULL,
email text,
password_hash bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( id ),
@ -546,7 +546,7 @@ CREATE TABLE users (
id BLOB NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
email TEXT,
password_hash BLOB NOT NULL,
created_at TIMESTAMP NOT NULL,
PRIMARY KEY ( id ),
@ -1917,13 +1917,17 @@ type User struct {
Id []byte
FirstName string
LastName string
Email string
Email *string
PasswordHash []byte
CreatedAt time.Time
}
func (User) _Table() string { return "users" }
type User_Create_Fields struct {
Email User_Email_Field
}
type User_Update_Fields struct {
FirstName User_FirstName_Field
LastName User_LastName_Field
@ -1991,13 +1995,26 @@ func (User_LastName_Field) _Column() string { return "last_name" }
type User_Email_Field struct {
_set bool
_null bool
_value string
_value *string
}
func User_Email(v string) User_Email_Field {
return User_Email_Field{_set: true, _value: v}
return User_Email_Field{_set: true, _value: &v}
}
func User_Email_Raw(v *string) User_Email_Field {
if v == nil {
return User_Email_Null()
}
return User_Email(*v)
}
func User_Email_Null() User_Email_Field {
return User_Email_Field{_set: true, _null: true}
}
func (f User_Email_Field) isnull() bool { return !f._set || f._null || f._value == nil }
func (f User_Email_Field) value() interface{} {
if !f._set || f._null {
return nil
@ -2729,15 +2746,15 @@ func (obj *postgresImpl) Create_User(ctx context.Context,
user_id User_Id_Field,
user_first_name User_FirstName_Field,
user_last_name User_LastName_Field,
user_email User_Email_Field,
user_password_hash User_PasswordHash_Field) (
user_password_hash User_PasswordHash_Field,
optional User_Create_Fields) (
user *User, err error) {
__now := obj.db.Hooks.Now().UTC()
__id_val := user_id.value()
__first_name_val := user_first_name.value()
__last_name_val := user_last_name.value()
__email_val := user_email.value()
__email_val := optional.Email.value()
__password_hash_val := user_password_hash.value()
__created_at_val := __now
@ -3401,10 +3418,17 @@ func (obj *postgresImpl) Get_User_By_Email(ctx context.Context,
user_email User_Email_Field) (
user *User, err error) {
var __embed_stmt = __sqlbundle_Literal("SELECT users.id, users.first_name, users.last_name, users.email, users.password_hash, users.created_at FROM users WHERE users.email = ?")
var __cond_0 = &__sqlbundle_Condition{Left: "users.email", Equal: true, Right: "?", Null: true}
var __embed_stmt = __sqlbundle_Literals{Join: "", SQLs: []__sqlbundle_SQL{__sqlbundle_Literal("SELECT users.id, users.first_name, users.last_name, users.email, users.password_hash, users.created_at FROM users WHERE "), __cond_0}}
var __values []interface{}
__values = append(__values, user_email.value())
__values = append(__values)
if !user_email.isnull() {
__cond_0.Null = false
__values = append(__values, user_email.value())
}
var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt)
obj.logStmt(__stmt, __values...)
@ -4876,15 +4900,15 @@ func (obj *sqlite3Impl) Create_User(ctx context.Context,
user_id User_Id_Field,
user_first_name User_FirstName_Field,
user_last_name User_LastName_Field,
user_email User_Email_Field,
user_password_hash User_PasswordHash_Field) (
user_password_hash User_PasswordHash_Field,
optional User_Create_Fields) (
user *User, err error) {
__now := obj.db.Hooks.Now().UTC()
__id_val := user_id.value()
__first_name_val := user_first_name.value()
__last_name_val := user_last_name.value()
__email_val := user_email.value()
__email_val := optional.Email.value()
__password_hash_val := user_password_hash.value()
__created_at_val := __now
@ -5563,10 +5587,17 @@ func (obj *sqlite3Impl) Get_User_By_Email(ctx context.Context,
user_email User_Email_Field) (
user *User, err error) {
var __embed_stmt = __sqlbundle_Literal("SELECT users.id, users.first_name, users.last_name, users.email, users.password_hash, users.created_at FROM users WHERE users.email = ?")
var __cond_0 = &__sqlbundle_Condition{Left: "users.email", Equal: true, Right: "?", Null: true}
var __embed_stmt = __sqlbundle_Literals{Join: "", SQLs: []__sqlbundle_SQL{__sqlbundle_Literal("SELECT users.id, users.first_name, users.last_name, users.email, users.password_hash, users.created_at FROM users WHERE "), __cond_0}}
var __values []interface{}
__values = append(__values, user_email.value())
__values = append(__values)
if !user_email.isnull() {
__cond_0.Null = false
__values = append(__values, user_email.value())
}
var __stmt = __sqlbundle_Render(obj.dialect, __embed_stmt)
obj.logStmt(__stmt, __values...)
@ -7419,14 +7450,14 @@ func (rx *Rx) Create_User(ctx context.Context,
user_id User_Id_Field,
user_first_name User_FirstName_Field,
user_last_name User_LastName_Field,
user_email User_Email_Field,
user_password_hash User_PasswordHash_Field) (
user_password_hash User_PasswordHash_Field,
optional User_Create_Fields) (
user *User, err error) {
var tx *Tx
if tx, err = rx.getTx(ctx); err != nil {
return
}
return tx.Create_User(ctx, user_id, user_first_name, user_last_name, user_email, user_password_hash)
return tx.Create_User(ctx, user_id, user_first_name, user_last_name, user_password_hash, optional)
}
@ -7975,8 +8006,8 @@ type Methods interface {
user_id User_Id_Field,
user_first_name User_FirstName_Field,
user_last_name User_LastName_Field,
user_email User_Email_Field,
user_password_hash User_PasswordHash_Field) (
user_password_hash User_PasswordHash_Field,
optional User_Create_Fields) (
user *User, err error)
Delete_AccountingRaw_By_Id(ctx context.Context,

View File

@ -91,7 +91,7 @@ CREATE TABLE users (
id bytea NOT NULL,
first_name text NOT NULL,
last_name text NOT NULL,
email text NOT NULL,
email text,
password_hash bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( id ),

View File

@ -91,7 +91,7 @@ CREATE TABLE users (
id BLOB NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
email TEXT,
password_hash BLOB NOT NULL,
created_at TIMESTAMP NOT NULL,
PRIMARY KEY ( id ),

View File

@ -377,7 +377,7 @@ func (m *lockedUsers) Delete(ctx context.Context, id uuid.UUID) error {
return m.db.Delete(ctx, id)
}
// Get is a method for querying user from the database by id
// Get is a method for querying user from the database by id.
func (m *lockedUsers) Get(ctx context.Context, id uuid.UUID) (*console.User, error) {
m.Lock()
defer m.Unlock()
@ -391,14 +391,14 @@ func (m *lockedUsers) GetByEmail(ctx context.Context, email string) (*console.Us
return m.db.GetByEmail(ctx, email)
}
// Insert is a method for inserting user into the database
// Insert is a method for inserting user into the database.
func (m *lockedUsers) Insert(ctx context.Context, user *console.User) (*console.User, error) {
m.Lock()
defer m.Unlock()
return m.db.Insert(ctx, user)
}
// Update is a method for updating user entity
// Update is a method for updating user entity.
func (m *lockedUsers) Update(ctx context.Context, user *console.User) error {
m.Lock()
defer m.Unlock()

View File

@ -46,12 +46,22 @@ func (users *users) Insert(ctx context.Context, user *console.User) (*console.Us
return nil, err
}
var email dbx.User_Email_Field
if user.Email != "" {
email = dbx.User_Email(user.Email)
} else {
email = dbx.User_Email_Null()
}
createdUser, err := users.db.Create_User(ctx,
dbx.User_Id(userID[:]),
dbx.User_FirstName(user.FirstName),
dbx.User_LastName(user.LastName),
dbx.User_Email(user.Email),
dbx.User_PasswordHash(user.PasswordHash))
dbx.User_PasswordHash(user.PasswordHash),
dbx.User_Create_Fields{
Email: email,
},
)
if err != nil {
return nil, err
@ -105,12 +115,17 @@ func userFromDBX(user *dbx.User) (*console.User, error) {
return nil, err
}
return &console.User{
result := console.User{
ID: id,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PasswordHash: user.PasswordHash,
CreatedAt: user.CreatedAt,
}, nil
}
if user.Email != nil {
result.Email = *user.Email
}
return &result, nil
}

View File

@ -145,7 +145,7 @@ func TestUserFromDbx(t *testing.T) {
Id: []byte("qweqwe"),
FirstName: "FirstName",
LastName: "LastName",
Email: "email@ukr.net",
Email: nil,
PasswordHash: []byte("ihqerfgnu238723huagsd"),
CreatedAt: time.Now(),
}