Replace DB with Store in service/user (#321)

This commit is contained in:
zhaoyang
2022-01-28 19:03:34 +08:00
committed by GitHub
parent 8246d8f8a7
commit cd215a8392
17 changed files with 885 additions and 284 deletions

1
go.mod
View File

@@ -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

7
go.sum
View File

@@ -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=

View File

@@ -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")
}

48
user/domain/store_key.go Normal file
View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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)
}
}

54
user/migrate/context.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

88
user/migrate/migrate.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

84
user/migrate/user/user.go Normal file
View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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 {