fix: make user creation errors helpful (#5382)

* fix: make user creation errors helpful

* fix linting and unit testing errors

* fix linting

* make zitadel config reusable

* fix human validations

* translate ssr errors

* make zitadel config reusable

* cover more translations for ssr

* handle email validation message centrally

* fix unit tests

* fix linting

* align signatures

* use more precise wording

* handle phone validation message centrally

* fix: return specific profile errors

* docs: edit comments

* fix unit tests

---------

Co-authored-by: Silvan <silvan.reusser@gmail.com>
This commit is contained in:
Elio Bischof
2023-03-14 20:20:38 +01:00
committed by GitHub
parent 9ff810eb92
commit e00cc187fa
79 changed files with 610 additions and 485 deletions

View File

@@ -65,10 +65,10 @@ type ExternalUser struct {
FirstName string
LastName string
NickName string
Email string
Email EmailAddress
IsEmailVerified bool
PreferredLanguage language.Tag
Phone string
Phone PhoneNumber
IsPhoneVerified bool
Metadatas []*Metadata
}

View File

@@ -4,6 +4,7 @@ import (
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/errors"
caos_errors "github.com/zitadel/zitadel/internal/errors"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
@@ -60,8 +61,22 @@ func (f Gender) Specified() bool {
return f > GenderUnspecified && f < genderCount
}
func (u *Human) IsValid() bool {
return u.Username != "" && u.Profile != nil && u.Profile.IsValid() && u.Email != nil && u.Email.IsValid() && u.Phone == nil || (u.Phone != nil && u.Phone.PhoneNumber != "" && u.Phone.IsValid())
func (u *Human) Normalize() error {
if u.Username == "" {
return errors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Username.Empty")
}
if err := u.Profile.Validate(); err != nil {
return err
}
if err := u.Email.Validate(); err != nil {
return err
}
if u.Phone != nil && u.Phone.PhoneNumber != "" {
if err := u.Phone.Normalize(); err != nil {
return err
}
}
return nil
}
func (u *Human) CheckDomainPolicy(policy *DomainPolicy) error {
@@ -69,7 +84,7 @@ func (u *Human) CheckDomainPolicy(policy *DomainPolicy) error {
return caos_errors.ThrowPreconditionFailed(nil, "DOMAIN-zSH7j", "Errors.Users.DomainPolicyNil")
}
if !policy.UserLoginMustBeDomain && u.Profile != nil && u.Username == "" && u.Email != nil {
u.Username = u.EmailAddress
u.Username = string(u.EmailAddress)
}
return nil
}

View File

@@ -2,20 +2,38 @@ package domain
import (
"regexp"
"strings"
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/errors"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
var (
EmailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
)
type EmailAddress string
func (e EmailAddress) Validate() error {
if e == "" {
return errors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty")
}
if !emailRegex.MatchString(string(e)) {
return errors.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid")
}
return nil
}
func (e EmailAddress) Normalize() EmailAddress {
return EmailAddress(strings.TrimSpace(string(e)))
}
type Email struct {
es_models.ObjectRoot
EmailAddress string
EmailAddress EmailAddress
IsEmailVerified bool
}
@@ -26,8 +44,11 @@ type EmailCode struct {
Expiry time.Duration
}
func (e *Email) IsValid() bool {
return e.EmailAddress != "" && EmailRegex.MatchString(e.EmailAddress)
func (e *Email) Validate() error {
if e == nil {
return errors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty")
}
return e.EmailAddress.Validate()
}
func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, error) {

View File

@@ -65,7 +65,7 @@ func TestEmailValid(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.args.email.IsValid()
result := tt.args.email.Validate() == nil
if result != tt.result {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, result)
}

View File

@@ -4,19 +4,31 @@ import (
"time"
"github.com/ttacon/libphonenumber"
"github.com/zitadel/zitadel/internal/crypto"
caos_errs "github.com/zitadel/zitadel/internal/errors"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
const (
defaultRegion = "CH"
)
const defaultRegion = "CH"
type PhoneNumber string
func (p PhoneNumber) Normalize() (PhoneNumber, error) {
if p == "" {
return p, caos_errs.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty")
}
phoneNr, err := libphonenumber.Parse(string(p), defaultRegion)
if err != nil {
return p, caos_errs.ThrowInvalidArgument(err, "PHONE-so0wa", "Errors.User.Phone.Invalid")
}
return PhoneNumber(libphonenumber.Format(phoneNr, libphonenumber.E164)), nil
}
type Phone struct {
es_models.ObjectRoot
PhoneNumber string
PhoneNumber PhoneNumber
IsPhoneVerified bool
}
@@ -27,17 +39,16 @@ type PhoneCode struct {
Expiry time.Duration
}
func (p *Phone) IsValid() bool {
err := p.formatPhone()
return p.PhoneNumber != "" && err == nil
}
func (p *Phone) formatPhone() error {
phoneNr, err := libphonenumber.Parse(p.PhoneNumber, defaultRegion)
if err != nil {
return caos_errs.ThrowInvalidArgument(nil, "EVENT-so0wa", "Errors.User.Phone.Invalid")
func (p *Phone) Normalize() error {
if p == nil {
return caos_errs.ThrowInvalidArgument(nil, "PHONE-YlbwO", "Errors.User.Phone.Empty")
}
p.PhoneNumber = libphonenumber.Format(phoneNr, libphonenumber.E164)
normalizedNumber, err := p.PhoneNumber.Normalize()
if err != nil {
return err
}
// Issue for avoiding mutating state: https://github.com/zitadel/zitadel/issues/5412
p.PhoneNumber = normalizedNumber
return nil
}

View File

@@ -94,10 +94,9 @@ func TestFormatPhoneNumber(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.phone.formatPhone()
if tt.errFunc == nil && tt.result.PhoneNumber != tt.args.phone.PhoneNumber {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.args.phone.PhoneNumber, tt.result.PhoneNumber)
normalized, err := tt.args.phone.PhoneNumber.Normalize()
if tt.errFunc == nil && tt.result.PhoneNumber != normalized {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.PhoneNumber, normalized)
}
if tt.errFunc != nil && !tt.errFunc(err) {
t.Errorf("got wrong err: %v ", err)

View File

@@ -3,6 +3,7 @@ package domain
import (
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/errors"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
@@ -19,8 +20,17 @@ type Profile struct {
LoginNames []string
}
func (p *Profile) IsValid() bool {
return p.FirstName != "" && p.LastName != ""
func (p *Profile) Validate() error {
if p == nil {
return errors.ThrowInvalidArgument(nil, "PROFILE-GPY3p", "Errors.User.Profile.Empty")
}
if p.FirstName == "" {
return errors.ThrowInvalidArgument(nil, "PROFILE-RF5z2", "Errors.User.Profile.FirstNameEmpty")
}
if p.LastName == "" {
return errors.ThrowInvalidArgument(nil, "PROFILE-DSUkN", "Errors.User.Profile.LastNameEmpty")
}
return nil
}
func AvatarURL(prefix, resourceOwner, key string) string {