mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:37:32 +00:00
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:
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user