package domain import ( "context" "encoding/json" "errors" "fmt" "net/url" "path" "strings" "sync" "time" "github.com/micro/micro/v3/service/config" microerr "github.com/micro/micro/v3/service/errors" "github.com/micro/micro/v3/service/logger" "github.com/micro/micro/v3/service/store" "github.com/micro/services/pkg/cache" user "github.com/micro/services/user/proto" "github.com/sendgrid/sendgrid-go" "github.com/sendgrid/sendgrid-go/helpers/mail" ) var ( ErrNotFound = errors.New("not found") ) type pw struct { Password string `json:"password"` Salt string `json:"salt"` } type verificationToken struct { UserID string `json:"userId"` Token string `json:"token"` } type passwordResetCode struct { Expires time.Time `json:"expires"` UserID string `json:"userId"` Code string `json:"code"` } type Domain struct { store store.Store sengridKey string fromEmail string } var ( // TODO: use the config to drive this value defaultSender = "noreply@email.m3ocontent.com" ) func New(st store.Store) *Domain { var key, email string cfg, err := config.Get("micro.user.sendgrid.api_key") if err == nil { key = cfg.String("") } cfg, err = config.Get("micro.user.sendgrid.from_email") if err == nil { email = cfg.String(defaultSender) } if len(key) == 0 { logger.Info("No email key found") } else { logger.Info("Email key found") } return &Domain{ sengridKey: key, store: st, fromEmail: email, } } func (domain *Domain) SendEmail(fromName, toAddress, toUsername, subject, textContent, token, redirctUrl, failureRedirectUrl string) error { if domain.sengridKey == "" { return fmt.Errorf("empty email api key") } from := mail.NewEmail(fromName, domain.fromEmail) to := mail.NewEmail(toUsername, toAddress) // set the text content textContent = strings.Replace(textContent, "$micro_verification_link", "https://user.m3o.com?token="+token+"&redirectUrl="+url.QueryEscape(redirctUrl)+"&failureRedirectUrl="+url.QueryEscape(failureRedirectUrl), -1) message := mail.NewSingleEmail(from, subject, to, textContent, "") // send the email client := sendgrid.NewSendClient(domain.sengridKey) response, err := client.Send(message) logger.Info(response) return err } func (domain *Domain) SavePasswordResetCode(ctx context.Context, userId, code string) (*passwordResetCode, error) { pwcode := passwordResetCode{ Expires: time.Now().Add(24 * time.Hour), UserID: userId, Code: code, } val, err := json.Marshal(pwcode) if err != nil { return nil, err } 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 { return domain.store.Delete(generatePasswordResetCodeStoreKey(ctx, userId, code)) } // ReadPasswordResetCode returns the user reset code func (domain *Domain) ReadPasswordResetCode(ctx context.Context, userId, code string) (*passwordResetCode, error) { records, err := domain.store.Read(generatePasswordResetCodeStoreKey(ctx, userId, code)) if err != nil { return nil, err } if len(records) == 0 { return nil, errors.New("password reset code not found") } resetCode := &passwordResetCode{} if err := json.Unmarshal(records[0].Value, resetCode); err != nil { return nil, err } // check the expiry if resetCode.Expires.Before(time.Now()) { return nil, errors.New("password reset code expired") } return resetCode, nil } func (domain *Domain) SendPasswordResetEmail(ctx context.Context, userId, codeStr, fromName, toAddress, toUsername, subject, textContent string) error { if domain.sengridKey == "" { return fmt.Errorf("empty email api key") } from := mail.NewEmail(fromName, domain.fromEmail) to := mail.NewEmail(toUsername, toAddress) // save the password reset code pw, err := domain.SavePasswordResetCode(ctx, userId, codeStr) if err != nil { return err } // set the code in the text content textContent = strings.Replace(textContent, "$code", pw.Code, -1) message := mail.NewSingleEmail(from, subject, to, textContent, "") // send the email client := sendgrid.NewSendClient(domain.sengridKey) response, err := client.Send(message) // log the response logger.Info(response) return err } func (domain *Domain) CreateSession(ctx context.Context, sess *user.Session) error { if sess.Created == 0 { sess.Created = time.Now().Unix() } if sess.Expires == 0 { sess.Expires = time.Now().Add(time.Hour * 24 * 7).Unix() } val, err := json.Marshal(sess) if err != nil { 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 { return domain.store.Delete(generateSessionStoreKey(ctx, id)) } // ReadToken returns the user id func (domain *Domain) ReadToken(ctx context.Context, email, token string) (string, error) { if token == "" { return "", errors.New("token id empty") } records, err := domain.store.Read(generateVerificationsTokenStoreKey(ctx, email, token)) if err != nil { return "", err } if len(records) == 0 { return "", errors.New("token not found") } 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, email, token string) (string, error) { tk, err := json.Marshal(verificationToken{ UserID: email, Token: token, }) if err != nil { return "", err } 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) { records, err := domain.store.Read(generateSessionStoreKey(ctx, id)) if err != nil { return nil, err } if len(records) == 0 { return nil, ErrNotFound } 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() // user account record accountVal, err := json.Marshal(user) if err != nil { return err } // password record passwordVal, err := json.Marshal(pw{ Password: password, Salt: salt, }) if err != nil { 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) } // 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 { // get old information of the user old, err := domain.Read(ctx, user.Id) if err != nil { 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) { 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 account = &user.Account{} var err error switch { case username != "": account, err = domain.SearchByUsername(ctx, username) case email != "": account, err = domain.SearchByEmail(ctx, email) } if err != nil { return []*user.Account{}, err } return []*user.Account{account}, nil } func (domain *Domain) UpdatePassword(ctx context.Context, userId string, salt string, password string) error { val, err := json.Marshal(pw{ Password: password, Salt: salt, }) if err != nil { 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) { records, err := domain.store.Read(generatePasswordStoreKey(ctx, userId)) if err != nil { return "", "", err } if len(records) == 0 { return "", "", ErrNotFound } 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 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(records) == 0 { return result, ErrNotFound } 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 } func (domain *Domain) CacheToken(ctx context.Context, token, email string, ttl int) error { expires := time.Now().Add(time.Duration(ttl) * time.Second) err := cache.Context(ctx).Set(token, email, expires) return err } func (domain *Domain) SendMLE(fromName, toAddress, toUsername, subject, textContent, token, address, endpoint string) error { if domain.sengridKey == "" { return fmt.Errorf("empty email api key") } from := mail.NewEmail(fromName, "support@m3o.com") to := mail.NewEmail(toUsername, toAddress) textContent = strings.Replace(textContent, "$micro_verification_link", fmt.Sprint("https://", path.Join(address, endpoint, token)), -1) message := mail.NewSingleEmail(from, subject, to, textContent, "") client := sendgrid.NewSendClient(domain.sengridKey) response, err := client.Send(message) logger.Info(response) return err } func (domain *Domain) CacheReadToken(ctx context.Context, token string) (string, error) { if token == "" { return "", errors.New("token empty") } var email string expires, err := cache.Context(ctx).Get(token, &email) if err != nil && err == cache.ErrNotFound { return "", errors.New("token not found") } else if err != nil { return "", microerr.InternalServerError("CacheReadToken", err.Error()) } if time.Until(expires).Seconds() < 0 { return "", errors.New("token expired") } return email, nil } func (domain *Domain) DeleteTenantData(tenantID string) error { keys, err := domain.store.List(store.ListPrefix(getStoreKeyPrefixForTenent(tenantID))) if err != nil { return err } for _, k := range keys { if err := domain.store.Delete(k); err != nil { return err } } logger.Infof("Deleted %d keys for user %s", len(keys), tenantID) return nil }