diff --git a/go.mod b/go.mod index 1bd18fc..47574dc 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/teamwork/test v0.0.0-20200108114543-02621bae84ad // indirect github.com/teamwork/utils v0.0.0-20211103135549-f7e7a68ba696 // indirect github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf + github.com/tidwall/gjson v1.12.1 github.com/tkuchiki/go-timezone v0.2.2 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect github.com/ttacon/libphonenumber v1.2.1 // indirect diff --git a/go.sum b/go.sum index de63513..d74626d 100644 --- a/go.sum +++ b/go.sum @@ -650,8 +650,13 @@ github.com/teamwork/utils v0.0.0-20211103135549-f7e7a68ba696 h1:G6Hc6/KxUmHxZsCg github.com/teamwork/utils v0.0.0-20211103135549-f7e7a68ba696/go.mod h1:3Fn0qxFeRNpvsg/9T1+btOOOKkd1qG2nPYKKcOmNpcs= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= -github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY= github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q= github.com/tkuchiki/go-timezone v0.2.2/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY= diff --git a/user/domain/domain.go b/user/domain/domain.go index 454019a..ea358d2 100644 --- a/user/domain/domain.go +++ b/user/domain/domain.go @@ -8,13 +8,14 @@ import ( "net/url" "path" "strings" + "sync" "time" - _struct "github.com/golang/protobuf/ptypes/struct" "github.com/micro/micro/v3/service/config" microerr "github.com/micro/micro/v3/service/errors" "github.com/micro/micro/v3/service/logger" - db "github.com/micro/services/db/proto" + "github.com/micro/micro/v3/service/store" + "github.com/micro/services/pkg/cache" user "github.com/micro/services/user/proto" @@ -27,26 +28,23 @@ var ( ) type pw struct { - ID string `json:"id"` Password string `json:"password"` Salt string `json:"salt"` } type verificationToken struct { - ID string `json:"id"` UserID string `json:"userId"` Token string `json:"token"` } type passwordResetCode struct { - ID string `json:"id"` Expires time.Time `json:"expires"` UserID string `json:"userId"` Code string `json:"code"` } type Domain struct { - db db.DbService + store store.Store sengridKey string fromEmail string } @@ -56,7 +54,7 @@ var ( defaultSender = "noreply@email.m3ocontent.com" ) -func New(db db.DbService) *Domain { +func New(st store.Store) *Domain { var key, email string cfg, err := config.Get("micro.user.sendgrid.api_key") if err == nil { @@ -73,7 +71,7 @@ func New(db db.DbService) *Domain { } return &Domain{ sengridKey: key, - db: db, + store: st, fromEmail: email, } } @@ -97,66 +95,50 @@ func (domain *Domain) SendEmail(fromName, toAddress, toUsername, subject, textCo return err } -func (domain *Domain) SavePasswordResetCode(ctx context.Context, userID, code string) (*passwordResetCode, error) { +func (domain *Domain) SavePasswordResetCode(ctx context.Context, userId, code string) (*passwordResetCode, error) { pwcode := passwordResetCode{ - ID: userID + "-" + code, Expires: time.Now().Add(24 * time.Hour), - UserID: userID, + UserID: userId, Code: code, } - s := &_struct.Struct{} - jso, _ := json.Marshal(pwcode) - err := s.UnmarshalJSON(jso) + val, err := json.Marshal(pwcode) if err != nil { return nil, err } - _, err = domain.db.Create(ctx, &db.CreateRequest{ - Table: "password-reset-codes", - Record: s, - }) + record := store.NewRecord(generatePasswordResetCodeStoreKey(ctx, userId, code), val) + err = domain.store.Write(record) return &pwcode, err } func (domain *Domain) DeletePasswordResetCode(ctx context.Context, userId, code string) error { - _, err := domain.db.Delete(ctx, &db.DeleteRequest{ - Table: "password-reset-codes", - Id: userId + "-" + code, - }) - return err + return domain.store.Delete(generatePasswordResetCodeStoreKey(ctx, userId, code)) } -// ReadToken returns the user id +// ReadPasswordResetCode returns the user reset code func (domain *Domain) ReadPasswordResetCode(ctx context.Context, userId, code string) (*passwordResetCode, error) { - // generate the key - id := userId + "-" + code - - if id == "" { - return nil, errors.New("password reset code id is empty") - } - token := &passwordResetCode{} - - rsp, err := domain.db.Read(ctx, &db.ReadRequest{ - Table: "password-reset-codes", - Query: fmt.Sprintf("id == '%v'", id), - }) + records, err := domain.store.Read(generatePasswordResetCodeStoreKey(ctx, userId, code)) if err != nil { return nil, err } - if len(rsp.Records) == 0 { + + if len(records) == 0 { return nil, errors.New("password reset code not found") } - m, _ := rsp.Records[0].MarshalJSON() - json.Unmarshal(m, token) + + resetCode := &passwordResetCode{} + if err := json.Unmarshal(records[0].Value, resetCode); err != nil { + return nil, err + } // check the expiry - if token.Expires.Before(time.Now()) { + if resetCode.Expires.Before(time.Now()) { return nil, errors.New("password reset code expired") } - return token, nil + return resetCode, nil } func (domain *Domain) SendPasswordResetEmail(ctx context.Context, userId, codeStr, fromName, toAddress, toUsername, subject, textContent string) error { @@ -196,274 +178,336 @@ func (domain *Domain) CreateSession(ctx context.Context, sess *user.Session) err sess.Expires = time.Now().Add(time.Hour * 24 * 7).Unix() } - s := &_struct.Struct{} - jso, _ := json.Marshal(sess) - err := s.UnmarshalJSON(jso) + val, err := json.Marshal(sess) if err != nil { return err } - _, err = domain.db.Create(ctx, &db.CreateRequest{ - Table: "sessions", - Record: s, - }) - return err + record := &store.Record{ + Key: generateSessionStoreKey(ctx, sess.Id), + Value: val, + } + + return domain.store.Write(record) } func (domain *Domain) DeleteSession(ctx context.Context, id string) error { - _, err := domain.db.Delete(ctx, &db.DeleteRequest{ - Table: "sessions", - Id: id, - }) - return err + return domain.store.Delete(generateSessionStoreKey(ctx, id)) } // ReadToken returns the user id -func (domain *Domain) ReadToken(ctx context.Context, userId, token string) (string, error) { - id := userId + "-" + token - +func (domain *Domain) ReadToken(ctx context.Context, email, token string) (string, error) { if token == "" { return "", errors.New("token id empty") } - tk := &verificationToken{} - - rsp, err := domain.db.Read(ctx, &db.ReadRequest{ - Table: "tokens", - Query: fmt.Sprintf("id == '%v'", id), - }) + records, err := domain.store.Read(generateVerificationsTokenStoreKey(ctx, email, token)) if err != nil { return "", err } - if len(rsp.Records) == 0 { + if len(records) == 0 { return "", errors.New("token not found") } - m, _ := rsp.Records[0].MarshalJSON() - json.Unmarshal(m, tk) - + tk := &verificationToken{} + err = json.Unmarshal(records[0].Value, tk) + if err != nil { + return "", err + } return tk.UserID, nil } // CreateToken returns the created and saved token -func (domain *Domain) CreateToken(ctx context.Context, userId, token string) (string, error) { - s := &_struct.Struct{} - jso, _ := json.Marshal(verificationToken{ - ID: userId + "-" + token, - UserID: userId, +func (domain *Domain) CreateToken(ctx context.Context, email, token string) (string, error) { + tk, err := json.Marshal(verificationToken{ + UserID: email, Token: token, }) - err := s.UnmarshalJSON(jso) if err != nil { return "", err } - _, err = domain.db.Create(ctx, &db.CreateRequest{ - Table: "tokens", - Record: s, - }) + record := &store.Record{ + Key: generateVerificationsTokenStoreKey(ctx, email, token), + Value: tk, + } + err = domain.store.Write(record) + if err != nil { + return "", err + } return token, err } func (domain *Domain) ReadSession(ctx context.Context, id string) (*user.Session, error) { - sess := &user.Session{} - if len(id) == 0 { - return nil, fmt.Errorf("no id provided") - } - q := fmt.Sprintf("id == '%v'", id) - logger.Infof("Running query: %v", q) - - rsp, err := domain.db.Read(ctx, &db.ReadRequest{ - Table: "sessions", - Query: q, - }) + records, err := domain.store.Read(generateSessionStoreKey(ctx, id)) if err != nil { return nil, err } - if len(rsp.Records) == 0 { + + if len(records) == 0 { return nil, ErrNotFound } - m, _ := rsp.Records[0].MarshalJSON() - json.Unmarshal(m, sess) + + sess := &user.Session{} + err = json.Unmarshal(records[0].Value, sess) + if err != nil { + return nil, err + } + return sess, nil } +// batchWrite write multiple records in batches +func (domain *Domain) batchWrite(records []*store.Record) error { + if len(records) == 0 { + return nil + } + + wg := sync.WaitGroup{} + lock := sync.Mutex{} + errs := make([]string, 0) + for _, v := range records { + wg.Add(1) + go func(r *store.Record) { + defer wg.Done() + if err := domain.store.Write(r); err != nil { + lock.Lock() + errs = append(errs, err.Error()) + lock.Unlock() + } + }(v) + } + wg.Wait() + + if len(errs) != 0 { + return errors.New(strings.Join(errs, ";")) + } + + return nil +} + func (domain *Domain) Create(ctx context.Context, user *user.Account, salt string, password string) error { user.Created = time.Now().Unix() user.Updated = time.Now().Unix() - s := &_struct.Struct{} - jso, _ := json.Marshal(user) - err := s.UnmarshalJSON(jso) - if err != nil { - return err - } - _, err = domain.db.Create(ctx, &db.CreateRequest{ - Table: "users", - Record: s, - }) + // user account record + accountVal, err := json.Marshal(user) if err != nil { return err } - pass := pw{ - ID: user.Id, + // password record + passwordVal, err := json.Marshal(pw{ Password: password, Salt: salt, - } - s = &_struct.Struct{} - jso, _ = json.Marshal(pass) - err = s.UnmarshalJSON(jso) + }) if err != nil { return err } - _, err = domain.db.Create(ctx, &db.CreateRequest{ - Table: "passwords", - Record: s, - }) - return err + records := []*store.Record{ + {Key: generateAccountStoreKey(ctx, user.Id), Value: accountVal}, + {Key: generateAccountUsernameStoreKey(ctx, user.Username), Value: accountVal}, + {Key: generateAccountEmailStoreKey(ctx, user.Email), Value: accountVal}, + {Key: generatePasswordStoreKey(ctx, user.Id), Value: passwordVal}, + } + + return domain.batchWrite(records) } -func (domain *Domain) Delete(ctx context.Context, id string) error { - _, err := domain.db.Delete(ctx, &db.DeleteRequest{ - Table: "users", - Id: id, - }) - return err +// batchDelete deletes the keys in batches +func (domain *Domain) batchDelete(keys []string) error { + if len(keys) == 0 { + return nil + } + + wg := sync.WaitGroup{} + lock := sync.Mutex{} + errs := make([]string, 0) + for _, key := range keys { + wg.Add(1) + go func(keyToDel string) { + defer wg.Done() + if err := domain.store.Delete(keyToDel); err != nil { + lock.Lock() + errs = append(errs, err.Error()) + lock.Unlock() + } + }(key) + + } + wg.Wait() + + if len(errs) != 0 { + return errors.New(strings.Join(errs, ";")) + } + + return nil +} + +func (domain *Domain) Delete(ctx context.Context, userId string) error { + account, err := domain.Read(ctx, userId) + if err != nil { + return err + } + + keys := []string{ + generateAccountStoreKey(ctx, userId), + generateAccountEmailStoreKey(ctx, account.Email), + generateAccountUsernameStoreKey(ctx, account.Username), + } + + return domain.batchDelete(keys) } func (domain *Domain) Update(ctx context.Context, user *user.Account) error { - user.Updated = time.Now().Unix() - - s := &_struct.Struct{} - jso, _ := json.Marshal(user) - err := s.UnmarshalJSON(jso) + // get old information of the user + old, err := domain.Read(ctx, user.Id) if err != nil { return err } - _, err = domain.db.Update(ctx, &db.UpdateRequest{ - Table: "users", - Record: s, - }) - return err + + keysToDelete := make([]string, 0) + if old.Email != user.Email { + keysToDelete = append(keysToDelete, generateAccountEmailStoreKey(ctx, old.Email)) + } + + if old.Username != user.Username { + keysToDelete = append(keysToDelete, generateAccountUsernameStoreKey(ctx, old.Username)) + } + + // update user + user.Created = old.Created + user.Updated = time.Now().Unix() + val, err := json.Marshal(user) + if err != nil { + return err + } + + records := []*store.Record{ + {Key: generateAccountStoreKey(ctx, user.Id), Value: val}, + {Key: generateAccountUsernameStoreKey(ctx, user.Username), Value: val}, + {Key: generateAccountEmailStoreKey(ctx, user.Email), Value: val}, + } + + // update + if err := domain.batchWrite(records); err != nil { + return err + } + + // delete + if err := domain.batchDelete(keysToDelete); err != nil { + return err + } + + return nil +} + +// readUserByKey read user account in store by key +func (domain *Domain) readUserByKey(ctx context.Context, key string) (*user.Account, error) { + var result = &user.Account{} + records, err := domain.store.Read(key) + if err != nil { + return result, err + } + + if len(records) == 0 { + return result, ErrNotFound + } + + err = json.Unmarshal(records[0].Value, result) + return result, err } func (domain *Domain) Read(ctx context.Context, userId string) (*user.Account, error) { - user := &user.Account{} - if len(userId) == 0 { - return nil, fmt.Errorf("no id provided") - } - q := fmt.Sprintf("id == '%v'", userId) - logger.Infof("Running query: %v", q) - rsp, err := domain.db.Read(ctx, &db.ReadRequest{ - Table: "users", - Query: q, - }) - if err != nil { - return nil, err - } - if len(rsp.Records) == 0 { - return nil, ErrNotFound - } - m, _ := rsp.Records[0].MarshalJSON() - json.Unmarshal(m, user) - return user, nil + return domain.readUserByKey(ctx, generateAccountStoreKey(ctx, userId)) +} + +func (domain *Domain) SearchByUsername(ctx context.Context, username string) (*user.Account, error) { + return domain.readUserByKey(ctx, generateAccountUsernameStoreKey(ctx, username)) +} + +func (domain *Domain) SearchByEmail(ctx context.Context, email string) (*user.Account, error) { + return domain.readUserByKey(ctx, generateAccountEmailStoreKey(ctx, email)) } func (domain *Domain) Search(ctx context.Context, username, email string) ([]*user.Account, error) { - var query string - if len(username) > 0 { - query = fmt.Sprintf("username == '%v'", username) - } else if len(email) > 0 { - query = fmt.Sprintf("email == '%v'", email) - } else { - return nil, errors.New("username and email cannot be blank") + var account = &user.Account{} + var err error + + switch { + case username != "": + account, err = domain.SearchByUsername(ctx, username) + case email != "": + account, err = domain.SearchByEmail(ctx, email) } - usr := &user.Account{} - - rsp, err := domain.db.Read(ctx, &db.ReadRequest{ - Table: "users", - Query: query, - }) if err != nil { - return nil, err + return []*user.Account{}, err } - if len(rsp.Records) == 0 { - return nil, ErrNotFound - } - m, _ := rsp.Records[0].MarshalJSON() - json.Unmarshal(m, usr) - return []*user.Account{usr}, nil + + return []*user.Account{account}, nil } -func (domain *Domain) UpdatePassword(ctx context.Context, id string, salt string, password string) error { - pass := pw{ - ID: id, +func (domain *Domain) UpdatePassword(ctx context.Context, userId string, salt string, password string) error { + val, err := json.Marshal(pw{ Password: password, Salt: salt, - } - s := &_struct.Struct{} - jso, _ := json.Marshal(pass) - err := s.UnmarshalJSON(jso) + }) + if err != nil { return err } - _, err = domain.db.Update(ctx, &db.UpdateRequest{ - Table: "passwords", - Record: s, - }) - return err + + record := &store.Record{ + Key: generatePasswordStoreKey(ctx, userId), + Value: val, + } + + return domain.store.Write(record) } func (domain *Domain) SaltAndPassword(ctx context.Context, userId string) (string, string, error) { - password := &pw{} - - rsp, err := domain.db.Read(ctx, &db.ReadRequest{ - Table: "passwords", - Query: fmt.Sprintf("id == '%v'", userId), - }) + records, err := domain.store.Read(generatePasswordStoreKey(ctx, userId)) if err != nil { return "", "", err } - if len(rsp.Records) == 0 { + if len(records) == 0 { return "", "", ErrNotFound } - m, _ := rsp.Records[0].MarshalJSON() - json.Unmarshal(m, password) + + password := &pw{} + if err := json.Unmarshal(records[0].Value, password); err != nil { + return "", "", err + } + return password.Salt, password.Password, nil } -func (domain *Domain) List(ctx context.Context, o, l int32) ([]*user.Account, error) { - var limit int32 = 25 - var offset int32 = 0 - if l > 0 { - limit = l - } - if o > 0 { - offset = o - } - rsp, err := domain.db.Read(ctx, &db.ReadRequest{ - Table: "users", - Limit: limit, - Offset: offset, - }) +func (domain *Domain) List(ctx context.Context, o, l uint32) (result []*user.Account, err error) { + records, err := domain.store.Read(generateAccountStoreKey(ctx, ""), + store.ReadPrefix(), + store.ReadLimit(uint(l)), + store.ReadLimit(uint(o))) + if err != nil { return nil, err } - if len(rsp.Records) == 0 { - return nil, ErrNotFound + + if len(records) == 0 { + return result, ErrNotFound } - ret := make([]*user.Account, len(rsp.Records)) - for i, v := range rsp.Records { - m, _ := v.MarshalJSON() - var user user.Account - json.Unmarshal(m, &user) - ret[i] = &user + + ret := make([]*user.Account, len(records)) + + for i, v := range records { + account := user.Account{} + json.Unmarshal(v.Value, &account) + ret[i] = &account } + return ret, nil } @@ -492,7 +536,6 @@ func (domain *Domain) SendMLE(fromName, toAddress, toUsername, subject, textCont } func (domain *Domain) CacheReadToken(ctx context.Context, token string) (string, error) { - if token == "" { return "", errors.New("token empty") } diff --git a/user/domain/store_key.go b/user/domain/store_key.go new file mode 100644 index 0000000..8ee8166 --- /dev/null +++ b/user/domain/store_key.go @@ -0,0 +1,48 @@ +package domain + +import ( + "context" + "fmt" + "strings" + + "github.com/micro/services/pkg/tenant" +) + +func getStoreKeyPrefix(ctx context.Context) string { + tenantId, ok := tenant.FromContext(ctx) + if !ok { + tenantId = "micro" + } + + tenantId = strings.Replace(strings.Replace(tenantId, "/", "_", -1), "-", "_", -1) + + return fmt.Sprintf("user/%s/", tenantId) +} + +func generateAccountStoreKey(ctx context.Context, userId string) string { + return fmt.Sprintf("%saccount/id/%s", getStoreKeyPrefix(ctx), userId) +} + +func generateAccountEmailStoreKey(ctx context.Context, email string) string { + return fmt.Sprintf("%sacccount/email/%s", getStoreKeyPrefix(ctx), email) +} + +func generateAccountUsernameStoreKey(ctx context.Context, username string) string { + return fmt.Sprintf("%saccount/username/%s", getStoreKeyPrefix(ctx), username) +} + +func generatePasswordStoreKey(ctx context.Context, userId string) string { + return fmt.Sprintf("%spassword/%s", getStoreKeyPrefix(ctx), userId) +} + +func generatePasswordResetCodeStoreKey(ctx context.Context, userId, code string) string { + return fmt.Sprintf("%spassword-reset-codes/%s-%s", getStoreKeyPrefix(ctx), userId, code) +} + +func generateSessionStoreKey(ctx context.Context, sessionId string) string { + return fmt.Sprintf("%ssession/%s", getStoreKeyPrefix(ctx), sessionId) +} + +func generateVerificationsTokenStoreKey(ctx context.Context, userId, token string) string { + return fmt.Sprintf("%sverification-token/%s-%s", getStoreKeyPrefix(ctx), userId, token) +} diff --git a/user/examples.json b/user/examples.json index d55cc63..afc798f 100644 --- a/user/examples.json +++ b/user/examples.json @@ -28,7 +28,7 @@ "title": "Update the account password", "run_check": false, "request": { - "id": "user-1", + "userId": "user-1", "oldPassword": "Password1", "newPassword": "Password2", "confirmPassword": "Password2" diff --git a/user/handler/handler.go b/user/handler/handler.go index 14a1f34..32ead74 100644 --- a/user/handler/handler.go +++ b/user/handler/handler.go @@ -11,12 +11,14 @@ import ( "github.com/google/uuid" "github.com/micro/micro/v3/service/errors" - db "github.com/micro/services/db/proto" + "github.com/micro/micro/v3/service/logger" + "github.com/micro/micro/v3/service/store" + "golang.org/x/crypto/bcrypt" + "golang.org/x/net/context" + otp "github.com/micro/services/otp/proto" "github.com/micro/services/user/domain" pb "github.com/micro/services/user/proto" - "golang.org/x/crypto/bcrypt" - "golang.org/x/net/context" ) const ( @@ -28,6 +30,7 @@ var ( emailFormat = regexp.MustCompile("^[\\w-\\.\\+]+@([\\w-]+\\.)+[\\w-]{2,4}$") ) +// random generate i length alphanum string func random(i int) string { bytes := make([]byte, i) for { @@ -45,38 +48,54 @@ type User struct { Otp otp.OtpService } -func NewUser(db db.DbService, otp otp.OtpService) *User { +func NewUser(st store.Store, otp otp.OtpService) *User { return &User{ - domain: domain.New(db), + domain: domain.New(st), Otp: otp, } } -func (s *User) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error { - if !emailFormat.MatchString(req.Email) { +// validatePostUserData checks userId, username, email post data are valid and in right format +func (s *User) validatePostUserData(ctx context.Context, userId, username, email string) error { + username = strings.TrimSpace(strings.ToLower(username)) + email = strings.TrimSpace(strings.ToLower(email)) + + if !emailFormat.MatchString(email) { return errors.BadRequest("create.email-format-check", "email has wrong format") } + + if userId == "" || username == "" || email == "" { + return errors.BadRequest("valid-check", "missing id or username or email") + } + + account, err := s.domain.SearchByUsername(ctx, username) + if err != nil && err.Error() != domain.ErrNotFound.Error() { + return err + } + + if account.Id != "" && account.Id != userId { + return errors.BadRequest("username-check", "username already exists") + } + + account, err = s.domain.SearchByEmail(ctx, email) + if err != nil && err.Error() != domain.ErrNotFound.Error() { + return err + } + if account.Id != "" && account.Id != userId { + return errors.BadRequest("email-check", "email already exists") + } + + return nil +} + +func (s *User) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error { if len(req.Password) < 8 { return errors.InternalServerError("user.Create.Check", "Password is less than 8 characters") } - req.Username = strings.ToLower(req.Username) - req.Email = strings.ToLower(req.Email) - usernames, err := s.domain.Search(ctx, req.Username, "") - if err != nil && err.Error() != "not found" { - return err - } - if len(usernames) > 0 { - return errors.BadRequest("create.username-check", "username already exists") - } - // TODO: don't error out here - emails, err := s.domain.Search(ctx, "", req.Email) - if err != nil && err.Error() != "not found" { + if err := s.validatePostUserData(ctx, req.Id, req.Username, req.Email); err != nil { return err } - if len(emails) > 0 { - return errors.BadRequest("create.email-check", "email already exists") - } salt := random(16) h, err := bcrypt.GenerateFromPassword([]byte(x+salt+req.Password), 10) @@ -90,8 +109,8 @@ func (s *User) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.Create acc := &pb.Account{ Id: req.Id, - Username: req.Username, - Email: req.Email, + Username: strings.ToLower(req.Username), + Email: strings.ToLower(req.Email), Profile: req.Profile, } @@ -107,26 +126,31 @@ func (s *User) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.Create } func (s *User) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error { + var account = &pb.Account{} + var err error + switch { case req.Id != "": - account, err := s.domain.Read(ctx, req.Id) - if err != nil { - return err - } - rsp.Account = account - return nil - case req.Username != "" || req.Email != "": - accounts, err := s.domain.Search(ctx, req.Username, req.Email) - if err != nil { - return err - } - rsp.Account = accounts[0] - return nil + account, err = s.domain.Read(ctx, req.Id) + case req.Username != "": + account, err = s.domain.SearchByUsername(ctx, req.Username) + case req.Email != "": + account, err = s.domain.SearchByEmail(ctx, req.Email) } + + rsp.Account = account + if err != nil { + return err + } + return nil } func (s *User) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error { + if err := s.validatePostUserData(ctx, req.Id, req.Username, req.Email); err != nil { + return err + } + return s.domain.Update(ctx, &pb.Account{ Id: req.Id, Username: strings.ToLower(req.Username), @@ -236,7 +260,7 @@ func (s *User) VerifyEmail(ctx context.Context, req *pb.VerifyEmailRequest, rsp } // check the token exists - userId, err := s.domain.ReadToken(ctx, req.Email, req.Token) + email, err := s.domain.ReadToken(ctx, req.Email, req.Token) if err != nil { return err } @@ -256,7 +280,7 @@ func (s *User) VerifyEmail(ctx context.Context, req *pb.VerifyEmailRequest, rsp } // mark user as verified - user, err := s.domain.Read(ctx, userId) + user, err := s.domain.SearchByEmail(ctx, email) if err != nil { return err } @@ -273,7 +297,7 @@ func (s *User) SendVerificationEmail(ctx context.Context, req *pb.SendVerificati } // search for the user - users, err := s.domain.Search(ctx, "", req.Email) + account, err := s.domain.SearchByEmail(ctx, req.Email) if err != nil { return err } @@ -294,7 +318,7 @@ func (s *User) SendVerificationEmail(ctx context.Context, req *pb.SendVerificati return err } - return s.domain.SendEmail(req.FromName, req.Email, users[0].Username, req.Subject, req.TextContent, token, req.RedirectUrl, req.FailureRedirectUrl) + return s.domain.SendEmail(req.FromName, req.Email, account.Username, req.Subject, req.TextContent, token, req.RedirectUrl, req.FailureRedirectUrl) } func (s *User) SendPasswordResetEmail(ctx context.Context, req *pb.SendPasswordResetEmailRequest, rsp *pb.SendPasswordResetEmailResponse) error { @@ -303,7 +327,7 @@ func (s *User) SendPasswordResetEmail(ctx context.Context, req *pb.SendPasswordR } // look for an existing user - users, err := s.domain.Search(ctx, "", req.Email) + account, err := s.domain.SearchByEmail(ctx, req.Email) if err != nil { return err } @@ -319,7 +343,7 @@ func (s *User) SendPasswordResetEmail(ctx context.Context, req *pb.SendPasswordR } // save the code in the database and then send via email - return s.domain.SendPasswordResetEmail(ctx, users[0].Id, resp.Code, req.FromName, req.Email, users[0].Username, req.Subject, req.TextContent) + return s.domain.SendPasswordResetEmail(ctx, account.Id, resp.Code, req.FromName, req.Email, account.Username, req.Subject, req.TextContent) } func (s *User) ResetPassword(ctx context.Context, req *pb.ResetPasswordRequest, rsp *pb.ResetPasswordResponse) error { @@ -340,13 +364,13 @@ func (s *User) ResetPassword(ctx context.Context, req *pb.ResetPasswordRequest, } // look for an existing user - users, err := s.domain.Search(ctx, "", req.Email) + account, err := s.domain.SearchByEmail(ctx, req.Email) if err != nil { return err } // check if a request was made to reset the password, we should have saved it - code, err := s.domain.ReadPasswordResetCode(ctx, users[0].Id, req.Code) + code, err := s.domain.ReadPasswordResetCode(ctx, account.Id, req.Code) if err != nil { return err } @@ -379,7 +403,7 @@ func (s *User) ResetPassword(ctx context.Context, req *pb.ResetPasswordRequest, } // delete our saved code - s.domain.DeletePasswordResetCode(ctx, users[0].Id, req.Code) + s.domain.DeletePasswordResetCode(ctx, account.Id, req.Code) return nil } @@ -403,7 +427,7 @@ func (s *User) SendMagicLink(ctx context.Context, req *pb.SendMagicLinkRequest, } // check if the email exist in the DB - users, err := s.domain.Search(ctx, "", req.Email) + account, err := s.domain.SearchByEmail(ctx, req.Email) if err != nil && err.Error() == "not found" { return errors.BadRequest("SendMagicLink.email-check", "email doesn't exist") } else if err != nil { @@ -419,12 +443,14 @@ func (s *User) SendMagicLink(ctx context.Context, req *pb.SendMagicLinkRequest, // save token, so we can retrieve it later err = s.domain.CacheToken(ctx, token, req.Email, ttl) if err != nil { + logger.Errorf("SendMagicLink.cacheToken error: %v", err) return errors.BadRequest("SendMagicLink.cacheToken", "Oooops something went wrong") } // send magic link to email address - err = s.domain.SendMLE(req.FromName, req.Email, users[0].Username, req.Subject, req.TextContent, token, req.Address, req.Endpoint) + err = s.domain.SendMLE(req.FromName, req.Email, account.Username, req.Subject, req.TextContent, token, req.Address, req.Endpoint) if err != nil { + logger.Errorf("SendMagicLink.cacheToken error: %v", err) return errors.BadRequest("SendMagicLink.sendEmail", "Oooops something went wrong") } @@ -456,23 +482,18 @@ func (s *User) VerifyToken(ctx context.Context, req *pb.VerifyTokenRequest, rsp } // save session - accounts, err := s.domain.Search(ctx, "", email) + account, err := s.domain.SearchByEmail(ctx, email) if err != nil { rsp.IsValid = false rsp.Message = "account not found" return err } - if len(accounts) == 0 { - rsp.IsValid = false - rsp.Message = "account not found" - return nil - } sess := &pb.Session{ Id: random(128), Created: time.Now().Unix(), Expires: time.Now().Add(time.Hour * 24 * 7).Unix(), - UserId: accounts[0].Id, + UserId: account.Id, } if err := s.domain.CreateSession(ctx, sess); err != nil { diff --git a/user/main.go b/user/main.go index 4a136bf..4075427 100644 --- a/user/main.go +++ b/user/main.go @@ -1,31 +1,71 @@ package main import ( + "time" + "github.com/micro/micro/v3/service" + "github.com/micro/micro/v3/service/config" "github.com/micro/micro/v3/service/logger" - db "github.com/micro/services/db/proto" + "github.com/micro/micro/v3/service/store" + "gorm.io/driver/postgres" + "gorm.io/gorm" + otp "github.com/micro/services/otp/proto" "github.com/micro/services/pkg/tracing" "github.com/micro/services/user/handler" + "github.com/micro/services/user/migrate" proto "github.com/micro/services/user/proto" ) +var pgxDsn = "postgresql://postgres:postgres@localhost:5432/db?sslmode=disable" + +func migrateData() { + startTime := time.Now() + logger.Info("start migrate ...") + defer func() { + logger.Infof("all migrations are finished, use time: %v", time.Since(startTime)) + }() + + // Connect to the database + cfg, err := config.Get("micro.db.database") + if err != nil { + logger.Fatalf("Error loading config: %v", err) + } + + dsn := cfg.String(pgxDsn) + gormDb, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + + if err != nil { + logger.Fatal("Failed to connect to ") + } + + migration := migrate.NewMigration(gormDb) + if err := migration.Do(); err != nil { + logger.Fatal("migrate error: ", err) + } + + return +} + func main() { - service := service.New( + srv := service.New( service.Name("user"), ) - service.Init() + srv.Init() + + // migration work + go migrateData() hd := handler.NewUser( - db.NewDbService("db", service.Client()), - otp.NewOtpService("otp", service.Client()), + store.DefaultStore, + otp.NewOtpService("otp", srv.Client()), ) - proto.RegisterUserHandler(service.Server(), hd) + proto.RegisterUserHandler(srv.Server(), hd) traceCloser := tracing.SetupOpentracing("user") defer traceCloser.Close() - if err := service.Run(); err != nil { + if err := srv.Run(); err != nil { logger.Fatal(err) } } diff --git a/user/migrate/context.go b/user/migrate/context.go new file mode 100644 index 0000000..3d34e8c --- /dev/null +++ b/user/migrate/context.go @@ -0,0 +1,54 @@ +package migrate + +import ( + "github.com/micro/micro/v3/service/logger" + "gorm.io/gorm" + + "github.com/micro/services/user/migrate/entity" +) + +type Migration interface { + Migrate([]*entity.Row) error +} + +type context struct { + db *gorm.DB + strategy Migration +} + +func NewContext(db *gorm.DB, strg Migration) *context { + return &context{ + db: db, + strategy: strg, + } +} + +func (c *context) Migrate(tableName string) error { + var count int64 + + db := c.db.Table(tableName) + + if err := db.Count(&count).Error; err != nil { + return err + } + + var offset, limit = 0, 1000 + + for offset = 0; offset < int(count); offset = offset + limit { + rows := make([]*entity.Row, 0) + + if err := db.Offset(offset).Limit(limit).Find(&rows).Error; err != nil { + logger.Errorf("migrate error, table:%v offset:%v limit:%v error:%v", tableName, offset, limit, err) + continue + } + + if err := c.strategy.Migrate(rows); err != nil { + logger.Errorf("migrate error, table:%v offset:%v limit:%v error:%v", tableName, offset, limit, err) + continue + } + } + + logger.Infof("migrate done, table: %v, rows count: %v", tableName, count) + + return nil +} diff --git a/user/migrate/entity/entity.go b/user/migrate/entity/entity.go new file mode 100644 index 0000000..511752a --- /dev/null +++ b/user/migrate/entity/entity.go @@ -0,0 +1,17 @@ +package entity + +import ( + "fmt" + "time" +) + +type Row struct { + Id string + Data string + CreatedAt time.Time + UpdatedAt time.Time +} + +func KeyPrefix(tenantId string) string { + return fmt.Sprintf("user/%s/", tenantId) +} diff --git a/user/migrate/migrate.go b/user/migrate/migrate.go new file mode 100644 index 0000000..6173b12 --- /dev/null +++ b/user/migrate/migrate.go @@ -0,0 +1,88 @@ +package migrate + +import ( + "strings" + "sync" + + "github.com/micro/micro/v3/service/logger" + "github.com/micro/micro/v3/service/store" + "github.com/pkg/errors" + "gorm.io/gorm" + + pwdMgr "github.com/micro/services/user/migrate/password" + resetMgr "github.com/micro/services/user/migrate/password_reset_code" + sessionMgr "github.com/micro/services/user/migrate/session" + tokenMgr "github.com/micro/services/user/migrate/token" + userMgr "github.com/micro/services/user/migrate/user" +) + +type migration struct { + db *gorm.DB + store store.Store +} + +func NewMigration(db *gorm.DB) *migration { + return &migration{ + db: db, + store: store.DefaultStore, + } +} + +func (m *migration) Do() error { + // get all tables + var tables []string + if err := m.db.Table("information_schema.tables"). + Where("table_schema = ?", "public"). + Pluck("table_name", &tables).Error; err != nil { + return errors.Wrap(err, "get pgx tables error") + } + + // migrate all data in concurrency + wg := sync.WaitGroup{} + // max concurrency is 5 + concurrencyChan := make(chan struct{}, 5) + + var strg Migration + + for _, t := range tables { + + wg.Add(1) + concurrencyChan <- struct{}{} + + go func(tableName string) { + defer func() { + wg.Done() + <-concurrencyChan + }() + + if strings.HasSuffix(tableName, "_users") { + strg = userMgr.New(m.store, strings.TrimSuffix(tableName, "_users")) + } else if strings.HasSuffix(tableName, "_passwords") { + strg = pwdMgr.New(m.store, strings.TrimSuffix(tableName, "_passwords")) + } else if strings.HasSuffix(tableName, "_sessions") { + strg = sessionMgr.New(m.store, strings.TrimSuffix(tableName, "_sessions")) + } else if strings.HasSuffix(tableName, "_tokens") { + strg = tokenMgr.New(m.store, strings.TrimSuffix(tableName, "_tokens")) + } else if strings.HasSuffix(tableName, "_password_reset_codes") { + strg = resetMgr.New(m.store, strings.TrimSuffix(tableName, "_password_reset_codes")) + } else { + logger.Infof("ignore table: %s", tableName) + } + + if strg == nil { + return + } + + ctx := NewContext(m.db, strg) + if err := ctx.Migrate(tableName); err != nil { + logger.Errorf("migrate table:%s error", tableName) + } + + }(t) + } + + wg.Wait() + + return nil + +} diff --git a/user/migrate/password/password.go b/user/migrate/password/password.go new file mode 100644 index 0000000..583126f --- /dev/null +++ b/user/migrate/password/password.go @@ -0,0 +1,50 @@ +package password + +import ( + "fmt" + + "github.com/micro/micro/v3/service/logger" + "github.com/micro/micro/v3/service/store" + "github.com/tidwall/gjson" + + "github.com/micro/services/user/migrate/entity" +) + +func generatePasswordStoreKey(tenantId string, id string) string { + return fmt.Sprintf("%spassword/%s", entity.KeyPrefix(tenantId), id) +} + +type password struct { + to store.Store + tenantId string +} + +func New(to store.Store, tenantId string) *password { + return &password{ + to: to, + tenantId: tenantId, + } +} + +func (u *password) migrate(rows []*entity.Row) error { + for _, rec := range rows { + id := gjson.Get(rec.Data, "id").String() + + key := generatePasswordStoreKey(u.tenantId, id) + err := u.to.Write(&store.Record{ + Key: key, + Value: []byte(rec.Data), + }) + + if err != nil { + logger.Errorf("migrate password write error: %v, %+v", err, key) + continue + } + } + + return nil +} + +func (u *password) Migrate(rows []*entity.Row) error { + return u.migrate(rows) +} diff --git a/user/migrate/password_reset_code/password_reset_code.go b/user/migrate/password_reset_code/password_reset_code.go new file mode 100644 index 0000000..f878910 --- /dev/null +++ b/user/migrate/password_reset_code/password_reset_code.go @@ -0,0 +1,50 @@ +package password_reset_code + +import ( + "fmt" + + "github.com/micro/micro/v3/service/logger" + "github.com/micro/micro/v3/service/store" + "github.com/tidwall/gjson" + + "github.com/micro/services/user/migrate/entity" +) + +func generatePasswordStoreKey(tenantId string, id string) string { + return fmt.Sprintf("%spassword-reset-codes/%s", entity.KeyPrefix(tenantId), id) +} + +type resetCode struct { + to store.Store + tenantId string +} + +func New(to store.Store, tenantId string) *resetCode { + return &resetCode{ + to: to, + tenantId: tenantId, + } +} + +func (u *resetCode) migrate(rows []*entity.Row) error { + for _, rec := range rows { + id := gjson.Get(rec.Data, "id").String() + + key := generatePasswordStoreKey(u.tenantId, id) + err := u.to.Write(&store.Record{ + Key: key, + Value: []byte(rec.Data), + }) + + if err != nil { + logger.Errorf("migrate password-reset-code write error: %v, %+v", err, key) + continue + } + } + + return nil +} + +func (u *resetCode) Migrate(rows []*entity.Row) error { + return u.migrate(rows) +} diff --git a/user/migrate/session/session.go b/user/migrate/session/session.go new file mode 100644 index 0000000..0f0f2ed --- /dev/null +++ b/user/migrate/session/session.go @@ -0,0 +1,50 @@ +package session + +import ( + "fmt" + + "github.com/micro/micro/v3/service/logger" + "github.com/micro/micro/v3/service/store" + "github.com/tidwall/gjson" + + "github.com/micro/services/user/migrate/entity" +) + +func generateStoreKey(tenantId string, id string) string { + return fmt.Sprintf("%ssession/%s", entity.KeyPrefix(tenantId), id) +} + +type session struct { + to store.Store + tenantId string +} + +func New(to store.Store, tenantId string) *session { + return &session{ + to: to, + tenantId: tenantId, + } +} + +func (s *session) migrate(rows []*entity.Row) error { + for _, rec := range rows { + id := gjson.Get(rec.Data, "id").String() + + key := generateStoreKey(s.tenantId, id) + err := s.to.Write(&store.Record{ + Key: key, + Value: []byte(rec.Data), + }) + + if err != nil { + logger.Errorf("migrate session write error: %v, %+v", err, key) + continue + } + } + + return nil +} + +func (s *session) Migrate(rows []*entity.Row) error { + return s.migrate(rows) +} diff --git a/user/migrate/token/token.go b/user/migrate/token/token.go new file mode 100644 index 0000000..dc266df --- /dev/null +++ b/user/migrate/token/token.go @@ -0,0 +1,50 @@ +package token + +import ( + "fmt" + + "github.com/micro/micro/v3/service/logger" + "github.com/micro/micro/v3/service/store" + "github.com/tidwall/gjson" + + "github.com/micro/services/user/migrate/entity" +) + +func generateStoreKey(tenantId string, id string) string { + return fmt.Sprintf("%sverification-token/%s", entity.KeyPrefix(tenantId), id) +} + +type token struct { + to store.Store + tenantId string +} + +func New(to store.Store, tenantId string) *token { + return &token{ + to: to, + tenantId: tenantId, + } +} + +func (s *token) migrate(rows []*entity.Row) error { + for _, rec := range rows { + id := gjson.Get(rec.Data, "id").String() + + key := generateStoreKey(s.tenantId, id) + err := s.to.Write(&store.Record{ + Key: key, + Value: []byte(rec.Data), + }) + + if err != nil { + logger.Errorf("migrate token write error: %v, %+v", err, key) + continue + } + } + + return nil +} + +func (s *token) Migrate(rows []*entity.Row) error { + return s.migrate(rows) +} diff --git a/user/migrate/user/user.go b/user/migrate/user/user.go new file mode 100644 index 0000000..31fd9fd --- /dev/null +++ b/user/migrate/user/user.go @@ -0,0 +1,84 @@ +package user + +import ( + "fmt" + "strings" + + "github.com/micro/micro/v3/service/logger" + "github.com/micro/micro/v3/service/store" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/micro/services/user/migrate/entity" +) + +func generateAccountStoreKey(tenantId, userId string) string { + return fmt.Sprintf("%saccount/id/%s", entity.KeyPrefix(tenantId), userId) +} + +func generateAccountEmailStoreKey(tenantId, email string) string { + return fmt.Sprintf("%sacccount/email/%s", entity.KeyPrefix(tenantId), email) +} + +func generateAccountUsernameStoreKey(tenantId, username string) string { + return fmt.Sprintf("%saccount/username/%s", entity.KeyPrefix(tenantId), username) +} + +type user struct { + to store.Store + tenantId string +} + +func New(to store.Store, tenantId string) *user { + return &user{ + to: to, + tenantId: tenantId, + } +} + +func (u *user) batchWrite(keys []string, val []byte) error { + errs := make([]string, 0) + + for _, key := range keys { + err := u.to.Write(&store.Record{ + Key: key, + Value: val, + }) + + if err != nil { + errs = append(errs, err.Error()) + } + + } + + if len(errs) > 0 { + return errors.New(strings.Join(errs, ";")) + } + + return nil +} + +func (u *user) migrate(rows []*entity.Row) error { + for _, rec := range rows { + id := gjson.Get(rec.Data, "id").String() + email := gjson.Get(rec.Data, "email").String() + username := gjson.Get(rec.Data, "username").String() + + keys := []string{ + generateAccountStoreKey(u.tenantId, id), + generateAccountEmailStoreKey(u.tenantId, email), + generateAccountUsernameStoreKey(u.tenantId, username), + } + + if err := u.batchWrite(keys, []byte(rec.Data)); err != nil { + logger.Errorf("migrate users batch write error: %v, %+v", err, keys) + continue + } + } + + return nil +} + +func (u *user) Migrate(rows []*entity.Row) error { + return u.migrate(rows) +} diff --git a/user/proto/user.pb.go b/user/proto/user.pb.go index 79c3444..577bb46 100644 --- a/user/proto/user.pb.go +++ b/user/proto/user.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.19.1 // source: proto/user.proto package user @@ -1529,10 +1529,10 @@ type ListRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Offset int32 `protobuf:"varint,1,opt,name=offset,proto3" json:"offset,omitempty"` + Offset uint32 `protobuf:"varint,1,opt,name=offset,proto3" json:"offset,omitempty"` // Maximum number of records to return. Default limit is 25. // Maximum limit is 1000. Anything higher will return an error. - Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + Limit uint32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` } func (x *ListRequest) Reset() { @@ -1567,14 +1567,14 @@ func (*ListRequest) Descriptor() ([]byte, []int) { return file_proto_user_proto_rawDescGZIP(), []int{26} } -func (x *ListRequest) GetOffset() int32 { +func (x *ListRequest) GetOffset() uint32 { if x != nil { return x.Offset } return 0 } -func (x *ListRequest) GetLimit() int32 { +func (x *ListRequest) GetLimit() uint32 { if x != nil { return x.Limit } @@ -2028,8 +2028,8 @@ var file_proto_user_proto_rawDesc = []byte{ 0x15, 0x52, 0x65, 0x73, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, - 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, + 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0x33, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, diff --git a/user/proto/user.proto b/user/proto/user.proto index 1cd0c30..2f5582a 100644 --- a/user/proto/user.proto +++ b/user/proto/user.proto @@ -230,10 +230,10 @@ message ResetPasswordResponse {} // List all users. Returns a paged list of results message ListRequest { - int32 offset = 1; + uint32 offset = 1; // Maximum number of records to return. Default limit is 25. // Maximum limit is 1000. Anything higher will return an error. - int32 limit = 2; + uint32 limit = 2; } message ListResponse {