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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 610 additions and 485 deletions

View File

@ -91,11 +91,11 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
if !mig.instanceSetup.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(mig.instanceSetup.Org.Human.Username, "@") { if !mig.instanceSetup.DomainPolicy.UserLoginMustBeDomain && !strings.Contains(mig.instanceSetup.Org.Human.Username, "@") {
mig.instanceSetup.Org.Human.Username = mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain) mig.instanceSetup.Org.Human.Username = mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)
} }
mig.instanceSetup.Org.Human.Email.Address = strings.TrimSpace(mig.instanceSetup.Org.Human.Email.Address) mig.instanceSetup.Org.Human.Email.Address = mig.instanceSetup.Org.Human.Email.Address.Normalize()
if mig.instanceSetup.Org.Human.Email.Address == "" { if mig.instanceSetup.Org.Human.Email.Address == "" {
mig.instanceSetup.Org.Human.Email.Address = mig.instanceSetup.Org.Human.Username mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username)
if !strings.Contains(mig.instanceSetup.Org.Human.Email.Address, "@") { if !strings.Contains(string(mig.instanceSetup.Org.Human.Email.Address), "@") {
mig.instanceSetup.Org.Human.Email.Address = mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain) mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain))
} }
} }

View File

@ -3,7 +3,8 @@ ExternalSecure: false
Database: Database:
cockroach: cockroach:
Host: db # This makes the e2e config reusable with an out-of-docker zitadel process and an /etc/hosts entry
Host: host.docker.internal
TLS: TLS:
Enabled: false Enabled: false

View File

@ -3,7 +3,8 @@ ExternalSecure: false
Database: Database:
cockroach: cockroach:
Host: db # This makes the e2e config reusable with an out-of-docker zitadel process and an /etc/hosts entry
Host: host.docker.internal
TLS: TLS:
Enabled: false Enabled: false

View File

@ -156,9 +156,9 @@ type human struct {
AvatarKey string AvatarKey string
PreferredLanguage string PreferredLanguage string
Gender domain.Gender Gender domain.Gender
Email string Email domain.EmailAddress
IsEmailVerified bool IsEmailVerified bool
Phone string Phone domain.PhoneNumber
IsPhoneVerified bool IsPhoneVerified bool
} }

View File

@ -564,13 +564,13 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w
} }
if user.Human.Email != "" { if user.Human.Email != "" {
dataUser.User.Email = &management_pb.ImportHumanUserRequest_Email{ dataUser.User.Email = &management_pb.ImportHumanUserRequest_Email{
Email: user.Human.Email, Email: string(user.Human.Email),
IsEmailVerified: user.Human.IsEmailVerified, IsEmailVerified: user.Human.IsEmailVerified,
} }
} }
if user.Human.Phone != "" { if user.Human.Phone != "" {
dataUser.User.Phone = &management_pb.ImportHumanUserRequest_Phone{ dataUser.User.Phone = &management_pb.ImportHumanUserRequest_Phone{
Phone: user.Human.Phone, Phone: string(user.Human.Phone),
IsPhoneVerified: user.Human.IsPhoneVerified, IsPhoneVerified: user.Human.IsPhoneVerified,
} }
} }

View File

@ -6,6 +6,7 @@ import (
user_grpc "github.com/zitadel/zitadel/internal/api/grpc/user" user_grpc "github.com/zitadel/zitadel/internal/api/grpc/user"
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
admin_grpc "github.com/zitadel/zitadel/pkg/grpc/admin" admin_grpc "github.com/zitadel/zitadel/pkg/grpc/admin"
) )
@ -29,7 +30,7 @@ func setUpOrgHumanToCommand(human *admin_grpc.SetUpOrgRequest_Human) *command.Ad
func setUpOrgHumanEmailToDomain(email *admin_grpc.SetUpOrgRequest_Human_Email) command.Email { func setUpOrgHumanEmailToDomain(email *admin_grpc.SetUpOrgRequest_Human_Email) command.Email {
return command.Email{ return command.Email{
Address: email.Email, Address: domain.EmailAddress(email.Email),
Verified: email.IsEmailVerified, Verified: email.IsEmailVerified,
} }
} }
@ -39,7 +40,7 @@ func setUpOrgHumanPhoneToDomain(phone *admin_grpc.SetUpOrgRequest_Human_Phone) c
return command.Phone{} return command.Phone{}
} }
return command.Phone{ return command.Phone{
Number: phone.Phone, Number: domain.PhoneNumber(phone.Phone),
Verified: phone.IsPhoneVerified, Verified: phone.IsPhoneVerified,
} }
} }

View File

@ -10,6 +10,6 @@ import (
func UpdateMyEmailToDomain(ctx context.Context, email *auth.SetMyEmailRequest) *domain.Email { func UpdateMyEmailToDomain(ctx context.Context, email *auth.SetMyEmailRequest) *domain.Email {
return &domain.Email{ return &domain.Email{
ObjectRoot: ctxToObjectRoot(ctx), ObjectRoot: ctxToObjectRoot(ctx),
EmailAddress: email.Email, EmailAddress: domain.EmailAddress(email.Email),
} }
} }

View File

@ -10,6 +10,6 @@ import (
func UpdateMyPhoneToDomain(ctx context.Context, phone *auth.SetMyPhoneRequest) *domain.Phone { func UpdateMyPhoneToDomain(ctx context.Context, phone *auth.SetMyPhoneRequest) *domain.Phone {
return &domain.Phone{ return &domain.Phone{
ObjectRoot: ctxToObjectRoot(ctx), ObjectRoot: ctxToObjectRoot(ctx),
PhoneNumber: phone.Phone, PhoneNumber: domain.PhoneNumber(phone.Phone),
} }
} }

View File

@ -208,7 +208,7 @@ func AddHumanUserRequestToAddHuman(req *mgmt_pb.AddHumanUserRequest) *command.Ad
NickName: req.Profile.NickName, NickName: req.Profile.NickName,
DisplayName: req.Profile.DisplayName, DisplayName: req.Profile.DisplayName,
Email: command.Email{ Email: command.Email{
Address: req.Email.Email, Address: domain.EmailAddress(req.Email.Email),
Verified: req.Email.IsEmailVerified, Verified: req.Email.IsEmailVerified,
}, },
PreferredLanguage: lang, PreferredLanguage: lang,
@ -221,7 +221,7 @@ func AddHumanUserRequestToAddHuman(req *mgmt_pb.AddHumanUserRequest) *command.Ad
} }
if req.Phone != nil { if req.Phone != nil {
human.Phone = command.Phone{ human.Phone = command.Phone{
Number: req.Phone.Phone, Number: domain.PhoneNumber(req.Phone.Phone),
Verified: req.Phone.IsPhoneVerified, Verified: req.Phone.IsPhoneVerified,
} }
} }
@ -446,7 +446,7 @@ func (s *Server) ResendHumanInitialization(ctx context.Context, req *mgmt_pb.Res
if err != nil { if err != nil {
return nil, err return nil, err
} }
details, err := s.command.ResendInitialMail(ctx, req.UserId, req.Email, authz.GetCtxData(ctx).OrgID, initCodeGenerator) details, err := s.command.ResendInitialMail(ctx, req.UserId, domain.EmailAddress(req.Email), authz.GetCtxData(ctx).OrgID, initCodeGenerator)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -91,37 +91,6 @@ func ListUserMetadataToDomain(req *mgmt_pb.ListUserMetadataRequest) (*query.User
}, nil }, nil
} }
func AddHumanUserRequestToDomain(req *mgmt_pb.AddHumanUserRequest) *domain.Human {
h := &domain.Human{
Username: req.UserName,
}
preferredLanguage, err := language.Parse(req.Profile.PreferredLanguage)
logging.Log("MANAG-M029f").OnError(err).Debug("language malformed")
h.Profile = &domain.Profile{
FirstName: req.Profile.FirstName,
LastName: req.Profile.LastName,
NickName: req.Profile.NickName,
DisplayName: req.Profile.DisplayName,
PreferredLanguage: preferredLanguage,
Gender: user_grpc.GenderToDomain(req.Profile.Gender),
}
h.Email = &domain.Email{
EmailAddress: req.Email.Email,
IsEmailVerified: req.Email.IsEmailVerified,
}
if req.Phone != nil {
h.Phone = &domain.Phone{
PhoneNumber: req.Phone.Phone,
IsPhoneVerified: req.Phone.IsPhoneVerified,
}
}
if req.InitialPassword != "" {
h.Password = &domain.Password{SecretString: req.InitialPassword, ChangeRequired: true}
}
return h
}
func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human *domain.Human, passwordless bool, links []*domain.UserIDPLink) { func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human *domain.Human, passwordless bool, links []*domain.UserIDPLink) {
human = &domain.Human{ human = &domain.Human{
Username: req.UserName, Username: req.UserName,
@ -137,12 +106,12 @@ func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human
Gender: user_grpc.GenderToDomain(req.Profile.Gender), Gender: user_grpc.GenderToDomain(req.Profile.Gender),
} }
human.Email = &domain.Email{ human.Email = &domain.Email{
EmailAddress: req.Email.Email, EmailAddress: domain.EmailAddress(req.Email.Email),
IsEmailVerified: req.Email.IsEmailVerified, IsEmailVerified: req.Email.IsEmailVerified,
} }
if req.Phone != nil { if req.Phone != nil {
human.Phone = &domain.Phone{ human.Phone = &domain.Phone{
PhoneNumber: req.Phone.Phone, PhoneNumber: domain.PhoneNumber(req.Phone.Phone),
IsPhoneVerified: req.Phone.IsPhoneVerified, IsPhoneVerified: req.Phone.IsPhoneVerified,
} }
} }
@ -199,7 +168,7 @@ func UpdateHumanEmailRequestToDomain(ctx context.Context, req *mgmt_pb.UpdateHum
AggregateID: req.UserId, AggregateID: req.UserId,
ResourceOwner: authz.GetCtxData(ctx).OrgID, ResourceOwner: authz.GetCtxData(ctx).OrgID,
}, },
EmailAddress: req.Email, EmailAddress: domain.EmailAddress(req.Email),
IsEmailVerified: req.IsEmailVerified, IsEmailVerified: req.IsEmailVerified,
} }
} }
@ -207,7 +176,7 @@ func UpdateHumanEmailRequestToDomain(ctx context.Context, req *mgmt_pb.UpdateHum
func UpdateHumanPhoneRequestToDomain(req *mgmt_pb.UpdateHumanPhoneRequest) *domain.Phone { func UpdateHumanPhoneRequestToDomain(req *mgmt_pb.UpdateHumanPhoneRequest) *domain.Phone {
return &domain.Phone{ return &domain.Phone{
ObjectRoot: models.ObjectRoot{AggregateID: req.UserId}, ObjectRoot: models.ObjectRoot{AggregateID: req.UserId},
PhoneNumber: req.Phone, PhoneNumber: domain.PhoneNumber(req.Phone),
IsPhoneVerified: req.IsPhoneVerified, IsPhoneVerified: req.IsPhoneVerified,
} }
} }

View File

@ -59,7 +59,7 @@ func CreateInstancePbToSetupInstance(req *system_pb.CreateInstanceRequest, defau
func createInstancePbToAddHuman(req *system_pb.CreateInstanceRequest_Human, defaultHuman command.AddHuman, userLoginMustBeDomain bool, org, externalDomain string) *command.AddHuman { func createInstancePbToAddHuman(req *system_pb.CreateInstanceRequest_Human, defaultHuman command.AddHuman, userLoginMustBeDomain bool, org, externalDomain string) *command.AddHuman {
user := defaultHuman user := defaultHuman
if req.Email != nil { if req.Email != nil {
user.Email.Address = req.Email.Email user.Email.Address = domain.EmailAddress(req.Email.Email)
user.Email.Verified = req.Email.IsEmailVerified user.Email.Verified = req.Email.IsEmailVerified
} }
if req.Profile != nil { if req.Profile != nil {
@ -164,7 +164,7 @@ func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInst
instance.Org.Human = new(command.AddHuman) instance.Org.Human = new(command.AddHuman)
} }
if req.OwnerEmail.Email != "" { if req.OwnerEmail.Email != "" {
instance.Org.Human.Email.Address = req.OwnerEmail.Email instance.Org.Human.Email.Address = domain.EmailAddress(req.OwnerEmail.Email)
instance.Org.Human.Email.Verified = req.OwnerEmail.IsEmailVerified instance.Org.Human.Email.Verified = req.OwnerEmail.IsEmailVerified
} }
if req.OwnerProfile != nil { if req.OwnerProfile != nil {

View File

@ -58,11 +58,11 @@ func HumanToPb(view *query.Human, assetPrefix, owner string) *user_pb.Human {
AvatarUrl: domain.AvatarURL(assetPrefix, owner, view.AvatarKey), AvatarUrl: domain.AvatarURL(assetPrefix, owner, view.AvatarKey),
}, },
Email: &user_pb.Email{ Email: &user_pb.Email{
Email: view.Email, Email: string(view.Email),
IsEmailVerified: view.IsEmailVerified, IsEmailVerified: view.IsEmailVerified,
}, },
Phone: &user_pb.Phone{ Phone: &user_pb.Phone{
Phone: view.Phone, Phone: string(view.Phone),
IsPhoneVerified: view.IsPhoneVerified, IsPhoneVerified: view.IsPhoneVerified,
}, },
} }
@ -91,7 +91,7 @@ func ProfileToPb(profile *query.Profile, assetPrefix string) *user_pb.Profile {
func EmailToPb(email *query.Email) *user_pb.Email { func EmailToPb(email *query.Email) *user_pb.Email {
return &user_pb.Email{ return &user_pb.Email{
Email: email.Email, Email: string(email.Email),
IsEmailVerified: email.IsVerified, IsEmailVerified: email.IsVerified,
} }
} }
@ -105,7 +105,7 @@ func PhoneToPb(phone *query.Phone) *user_pb.Phone {
func ModelEmailToPb(email *query.Email) *user_pb.Email { func ModelEmailToPb(email *query.Email) *user_pb.Email {
return &user_pb.Email{ return &user_pb.Email{
Email: email.Email, Email: string(email.Email),
IsEmailVerified: email.IsVerified, IsEmailVerified: email.IsVerified,
} }
} }

View File

@ -268,7 +268,7 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSette
if user.Human == nil { if user.Human == nil {
continue continue
} }
userInfo.SetEmail(user.Human.Email, user.Human.IsEmailVerified) userInfo.SetEmail(string(user.Human.Email), user.Human.IsEmailVerified)
case oidc.ScopeProfile: case oidc.ScopeProfile:
userInfo.SetPreferredUsername(user.PreferredLoginName) userInfo.SetPreferredUsername(user.PreferredLoginName)
userInfo.SetUpdatedAt(user.ChangeDate) userInfo.SetUpdatedAt(user.ChangeDate)
@ -287,7 +287,7 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSette
if user.Human == nil { if user.Human == nil {
continue continue
} }
userInfo.SetPhone(user.Human.Phone, user.Human.IsPhoneVerified) userInfo.SetPhone(string(user.Human.Phone), user.Human.IsPhoneVerified)
case oidc.ScopeAddress: case oidc.ScopeAddress:
//TODO: handle address for human users as soon as implemented //TODO: handle address for human users as soon as implemented
case ScopeUserMetaData: case ScopeUserMetaData:

View File

@ -154,7 +154,7 @@ func setUserinfo(user *query.User, userinfo models.AttributeSetter, attributes [
if user.Human == nil { if user.Human == nil {
return return
} }
userinfo.SetEmail(user.Human.Email) userinfo.SetEmail(string(user.Human.Email))
userinfo.SetSurname(user.Human.LastName) userinfo.SetSurname(user.Human.LastName)
userinfo.SetGivenName(user.Human.FirstName) userinfo.SetGivenName(user.Human.FirstName)
userinfo.SetFullName(user.Human.DisplayName) userinfo.SetFullName(user.Human.DisplayName)
@ -164,7 +164,7 @@ func setUserinfo(user *query.User, userinfo models.AttributeSetter, attributes [
switch attribute { switch attribute {
case provider.AttributeEmail: case provider.AttributeEmail:
if user.Human != nil { if user.Human != nil {
userinfo.SetEmail(user.Human.Email) userinfo.SetEmail(string(user.Human.Email))
} }
case provider.AttributeSurname: case provider.AttributeSurname:
if user.Human != nil { if user.Human != nil {

View File

@ -55,13 +55,13 @@ func (l *Login) runPostExternalAuthenticationActions(
actions.SetFields("setPreferredUsername", func(username string) { actions.SetFields("setPreferredUsername", func(username string) {
user.PreferredUsername = username user.PreferredUsername = username
}), }),
actions.SetFields("setEmail", func(email string) { actions.SetFields("setEmail", func(email domain.EmailAddress) {
user.Email = email user.Email = email
}), }),
actions.SetFields("setEmailVerified", func(verified bool) { actions.SetFields("setEmailVerified", func(verified bool) {
user.IsEmailVerified = verified user.IsEmailVerified = verified
}), }),
actions.SetFields("setPhone", func(phone string) { actions.SetFields("setPhone", func(phone domain.PhoneNumber) {
user.Phone = phone user.Phone = phone
}), }),
actions.SetFields("setPhoneVerified", func(verified bool) { actions.SetFields("setPhoneVerified", func(verified bool) {
@ -222,7 +222,7 @@ func (l *Login) runPreCreationActions(
actions.SetFields("setUsername", func(username string) { actions.SetFields("setUsername", func(username string) {
user.Username = username user.Username = username
}), }),
actions.SetFields("setEmail", func(email string) { actions.SetFields("setEmail", func(email domain.EmailAddress) {
if user.Email == nil { if user.Email == nil {
user.Email = &domain.Email{} user.Email = &domain.Email{}
} }
@ -234,11 +234,11 @@ func (l *Login) runPreCreationActions(
} }
user.Email.IsEmailVerified = verified user.Email.IsEmailVerified = verified
}), }),
actions.SetFields("setPhone", func(email string) { actions.SetFields("setPhone", func(phone domain.PhoneNumber) {
if user.Phone == nil { if user.Phone == nil {
user.Phone = &domain.Phone{} user.Phone = &domain.Phone{}
} }
user.Phone.PhoneNumber = email user.Phone.PhoneNumber = phone
}), }),
actions.SetFields("setPhoneVerified", func(verified bool) { actions.SetFields("setPhoneVerified", func(verified bool) {
if user.Phone == nil { if user.Phone == nil {

View File

@ -60,28 +60,28 @@ type externalNotFoundOptionData struct {
ShowUsername bool ShowUsername bool
ShowUsernameSuffix bool ShowUsernameSuffix bool
OrgRegister bool OrgRegister bool
ExternalEmail string ExternalEmail domain.EmailAddress
ExternalEmailVerified bool ExternalEmailVerified bool
ExternalPhone string ExternalPhone domain.PhoneNumber
ExternalPhoneVerified bool ExternalPhoneVerified bool
} }
type externalRegisterFormData struct { type externalRegisterFormData struct {
ExternalIDPConfigID string `schema:"external-idp-config-id"` ExternalIDPConfigID string `schema:"external-idp-config-id"`
ExternalIDPExtUserID string `schema:"external-idp-ext-user-id"` ExternalIDPExtUserID string `schema:"external-idp-ext-user-id"`
ExternalIDPDisplayName string `schema:"external-idp-display-name"` ExternalIDPDisplayName string `schema:"external-idp-display-name"`
ExternalEmail string `schema:"external-email"` ExternalEmail domain.EmailAddress `schema:"external-email"`
ExternalEmailVerified bool `schema:"external-email-verified"` ExternalEmailVerified bool `schema:"external-email-verified"`
Email string `schema:"email"` Email domain.EmailAddress `schema:"email"`
Username string `schema:"username"` Username string `schema:"username"`
Firstname string `schema:"firstname"` Firstname string `schema:"firstname"`
Lastname string `schema:"lastname"` Lastname string `schema:"lastname"`
Nickname string `schema:"nickname"` Nickname string `schema:"nickname"`
ExternalPhone string `schema:"external-phone"` ExternalPhone domain.PhoneNumber `schema:"external-phone"`
ExternalPhoneVerified bool `schema:"external-phone-verified"` ExternalPhoneVerified bool `schema:"external-phone-verified"`
Phone string `schema:"phone"` Phone domain.PhoneNumber `schema:"phone"`
Language string `schema:"language"` Language string `schema:"language"`
TermsConfirm bool `schema:"terms-confirm"` TermsConfirm bool `schema:"terms-confirm"`
} }
// handleExternalLoginStep is called as nextStep // handleExternalLoginStep is called as nextStep
@ -815,7 +815,7 @@ func mapExternalNotFoundOptionFormDataToLoginUser(formData *externalNotFoundOpti
IDPConfigID: formData.ExternalIDPConfigID, IDPConfigID: formData.ExternalIDPConfigID,
ExternalUserID: formData.ExternalIDPExtUserID, ExternalUserID: formData.ExternalIDPExtUserID,
PreferredUsername: formData.Username, PreferredUsername: formData.Username,
DisplayName: formData.Email, DisplayName: string(formData.Email),
FirstName: formData.Firstname, FirstName: formData.Firstname,
LastName: formData.Lastname, LastName: formData.Lastname,
NickName: formData.Nickname, NickName: formData.Nickname,

View File

@ -16,14 +16,14 @@ const (
) )
type registerFormData struct { type registerFormData struct {
Email string `schema:"email"` Email domain.EmailAddress `schema:"email"`
Username string `schema:"username"` Username string `schema:"username"`
Firstname string `schema:"firstname"` Firstname string `schema:"firstname"`
Lastname string `schema:"lastname"` Lastname string `schema:"lastname"`
Language string `schema:"language"` Language string `schema:"language"`
Password string `schema:"register-password"` Password string `schema:"register-password"`
Password2 string `schema:"register-password-confirmation"` Password2 string `schema:"register-password-confirmation"`
TermsConfirm bool `schema:"terms-confirm"` TermsConfirm bool `schema:"terms-confirm"`
} }
type registerData struct { type registerData struct {

View File

@ -94,7 +94,7 @@ func (l *Login) passLoginHintToRegistration(r *http.Request, authReq *domain.Aut
if authReq == nil { if authReq == nil {
return data return data
} }
data.Email = authReq.LoginHint data.Email = domain.EmailAddress(authReq.LoginHint)
domainPolicy, err := l.getOrgDomainPolicy(r, authReq.RequestedOrgID) domainPolicy, err := l.getOrgDomainPolicy(r, authReq.RequestedOrgID)
if err != nil { if err != nil {
logging.WithFields("authRequest", authReq.ID, "org", authReq.RequestedOrgID).Error("unable to load domain policy for registration loginHint") logging.WithFields("authRequest", authReq.ID, "org", authReq.RequestedOrgID).Error("unable to load domain policy for registration loginHint")

View File

@ -14,14 +14,14 @@ const (
) )
type registerOrgFormData struct { type registerOrgFormData struct {
RegisterOrgName string `schema:"orgname"` RegisterOrgName string `schema:"orgname"`
Email string `schema:"email"` Email domain.EmailAddress `schema:"email"`
Username string `schema:"username"` Username string `schema:"username"`
Firstname string `schema:"firstname"` Firstname string `schema:"firstname"`
Lastname string `schema:"lastname"` Lastname string `schema:"lastname"`
Password string `schema:"register-password"` Password string `schema:"register-password"`
Password2 string `schema:"register-password-confirmation"` Password2 string `schema:"register-password-confirmation"`
TermsConfirm bool `schema:"terms-confirm"` TermsConfirm bool `schema:"terms-confirm"`
} }
type registerOrgData struct { type registerOrgData struct {
@ -121,7 +121,7 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe
func (d registerOrgFormData) toUserDomain() *domain.Human { func (d registerOrgFormData) toUserDomain() *domain.Human {
if d.Username == "" { if d.Username == "" {
d.Username = d.Email d.Username = string(d.Email)
} }
return &domain.Human{ return &domain.Human{
Username: d.Username, Username: d.Username,
@ -140,7 +140,7 @@ func (d registerOrgFormData) toUserDomain() *domain.Human {
func (d registerOrgFormData) toCommandOrg() *command.OrgSetup { func (d registerOrgFormData) toCommandOrg() *command.OrgSetup {
if d.Username == "" { if d.Username == "" {
d.Username = d.Email d.Username = string(d.Email)
} }
return &command.OrgSetup{ return &command.OrgSetup{
Name: d.RegisterOrgName, Name: d.RegisterOrgName,

View File

@ -328,6 +328,33 @@ Errors:
Invalid: Userdaten sind ungültig Invalid: Userdaten sind ungültig
DomainNotAllowedAsUsername: Domäne ist bereits reserviert und kann nicht verwendet werden DomainNotAllowedAsUsername: Domäne ist bereits reserviert und kann nicht verwendet werden
NotAllowedToLink: Der Benutzer darf nicht mit einem externen Login Provider verlinkt werden NotAllowedToLink: Der Benutzer darf nicht mit einem externen Login Provider verlinkt werden
Profile:
NotFound: Profil nicht gefunden
NotChanged: Profil nicht verändert
Empty: Profil ist leer
FirstNameEmpty: Vorname im Profil ist leer
LastNameEmpty: Nachname im Profil ist leer
IDMissing: Profil ID fehlt
Email:
NotFound: Email nicht gefunden
Invalid: Email ist ungültig
AlreadyVerified: Email ist bereits verifiziert
NotChanged: Email wurde nicht geändert
Empty: Email ist leer
IDMissing: Email ID fehlt
Phone:
NotFound: Telefonnummer nicht gefunden
Invalid: Telefonnummer ist ungültig
AlreadyVerified: Telefonnummer bereits verifiziert
Empty: Telefonnummer ist leer
NotChanged: Telefonnummer wurde nicht geändert
Address:
NotFound: Adresse nicht gefunden
NotChanged: Adresse wurde nicht geändert
Username:
AlreadyExists: Benutzername ist bereits vergeben
Reserved: Benutzername ist bereits vergeben
Empty: Benutzername ist leer
Password: Password:
ConfirmationWrong: Passwort Bestätigung stimmt nicht überein ConfirmationWrong: Passwort Bestätigung stimmt nicht überein
Empty: Passwort ist leer Empty: Passwort ist leer

View File

@ -328,6 +328,33 @@ Errors:
Invalid: Invalid userdata Invalid: Invalid userdata
DomainNotAllowedAsUsername: Domain is already reserved and cannot be used DomainNotAllowedAsUsername: Domain is already reserved and cannot be used
NotAllowedToLink: User is not allowed to link with external login provider NotAllowedToLink: User is not allowed to link with external login provider
Profile:
NotFound: Profile not found
NotChanged: Profile not changed
Empty: Profile is empty
FirstNameEmpty: First name in profile is empty
LastNameEmpty: Last name in profile is empty
IDMissing: Profile ID is missing
Email:
NotFound: Email not found
Invalid: Email is invalid
AlreadyVerified: Email is already verified
NotChanged: Email not changed
Empty: Email is empty
IDMissing: Email ID is missing
Phone:
NotFound: Phone not found
Invalid: Phone is invalid
AlreadyVerified: Phone already verified
Empty: Phone is empty
NotChanged: Phone not changed
Address:
NotFound: Address not found
NotChanged: Address not changed
Username:
AlreadyExists: Username already taken
Reserved: Username is already taken
Empty: Username is empty
Password: Password:
ConfirmationWrong: Passwordconfirmation is wrong ConfirmationWrong: Passwordconfirmation is wrong
Empty: Password is empty Empty: Password is empty

View File

@ -328,6 +328,33 @@ Errors:
Invalid: Données utilisateur non valides Invalid: Données utilisateur non valides
DomainNotAllowedAsUsername: Le domaine est déjà réservé et ne peut pas être utilisé. DomainNotAllowedAsUsername: Le domaine est déjà réservé et ne peut pas être utilisé.
NotAllowedToLink: L'utilisateur n'est pas autorisé à établir un lien avec un fournisseur de connexion externe NotAllowedToLink: L'utilisateur n'est pas autorisé à établir un lien avec un fournisseur de connexion externe
Profile:
NotFound: Profil non trouvé
NotChanged: Le profil n'a pas changé
Empty: Profil est vide
FirstNameEmpty: Le prénom dans le profil est vide
LastNameEmpty: Le nom de famille dans le profil est vide
IDMissing: Profil ID manquant
Email:
NotFound: Email non trouvé
Invalid: L'email n'est pas valide
AlreadyVerified: L'adresse électronique est déjà vérifiée
NotChanged: L'adresse électronique n'a pas changé
Empty: Email est vide
IDMissing: Email ID manquant
Phone:
Notfound: Téléphone non trouvé
Invalid: Le téléphone n'est pas valide
AlreadyVerified: Téléphone déjà vérifié
Empty: Téléphone est vide
NotChanged: Téléphone n'a pas changé
Address:
NotFound: Adresse non trouvée
NotChanged: L'adresse n'a pas changé
Username:
AlreadyExists: Nom d'utilisateur déjà pris
Reserved: Le nom d'utilisateur est déjà pris
Empty: Le nom d'utilisateur est vide
Password: Password:
ConfirmationWrong: La confirmation du mot de passe est erronée ConfirmationWrong: La confirmation du mot de passe est erronée
Empty: Le mot de passe est vide Empty: Le mot de passe est vide

View File

@ -328,6 +328,33 @@ Errors:
Invalid: I dati del utente non sono validi Invalid: I dati del utente non sono validi
DomainNotAllowedAsUsername: Il dominio è già riservato e non può essere utilizzato DomainNotAllowedAsUsername: Il dominio è già riservato e non può essere utilizzato
NotAllowedToLink: L'utente non è autorizzato a collegarsi con un provider di accesso esterno NotAllowedToLink: L'utente non è autorizzato a collegarsi con un provider di accesso esterno
Profile:
NotFound: Profilo non trovato
NotChanged: Profilo non cambiato
Empty: Profilo è vuoto
FirstNameEmpty: Il nome nel profilo è vuoto
LastNameEmpty: Il cognome nel profilo è vuoto
IDMissing: Profilo ID mancante
Email:
NotFound: Email non trovata
Invalid: L'e-mail non è valida
AlreadyVerified: L'e-mail è già verificata
NotChanged: Email non cambiata
Empty: Email è vuota
IDMissing: Email ID mancante
Phone:
NotFound: Telefono non trovato
Invalid: Il telefono non è valido
AlreadyVerified: Telefono già verificato
Empty: Il telefono è vuoto
NotChanged: Telefono non cambiato
Address:
NotFound: Indirizzo non trovato
NotChanged: Indirizzo non cambiato
Username:
AlreadyExists: Nome utente già preso
Reserved: Il nome utente è già preso
Empty: Il nome utente è vuoto
Password: Password:
ConfirmationWrong: La conferma della password è sbagliata ConfirmationWrong: La conferma della password è sbagliata
Empty: La password è vuota Empty: La password è vuota

View File

@ -328,6 +328,33 @@ Errors:
Invalid: Nieprawidłowe dane użytkownika Invalid: Nieprawidłowe dane użytkownika
DomainNotAllowedAsUsername: Domena jest już zarezerwowana i nie może być użyta DomainNotAllowedAsUsername: Domena jest już zarezerwowana i nie może być użyta
NotAllowedToLink: Użytkownik nie jest upoważniony do łączenia z zewnętrznym dostawcą logowania NotAllowedToLink: Użytkownik nie jest upoważniony do łączenia z zewnętrznym dostawcą logowania
Profile:
NotFound: Profil nie znaleziony
NotChanged: Profil nie zmieniony
Empty: Profil jest pusty
FirstNameEmpty: Imię w profilu jest puste
LastNameEmpty: Nazwisko w profilu jest puste
IDMissing: Profil ID brakuje
Email:
NotFound: Adres e-mail nie znaleziony
Invalid: Adres e-mail jest nieprawidłowy
AlreadyVerified: Adres e-mail jest już zweryfikowany
NotChanged: Adres e-mail nie zmieniony
Empty: Adres e-mail jest pusty
IDMissing: Adres e-mail ID brakuje
Phone:
NotFound: Numer telefonu nie znaleziony
Invalid: Numer telefonu jest nieprawidłowy
AlreadyVerified: Numer telefonu już zweryfikowany
Empty: Numer telefonu jest pusty
NotChanged: Numer telefonu nie zmieniony
Address:
NotFound: Adres nie znaleziony
NotChanged: Adres nie zmieniony
Username:
AlreadyExists: Nazwa użytkownika jest już zajęta
Reserved: Nazwa użytkownika jest już zajęta
Empty: Nazwa użytkownika jest pusty
Password: Password:
ConfirmationWrong: Potwierdzenie hasła jest niepoprawne ConfirmationWrong: Potwierdzenie hasła jest niepoprawne
Empty: Hasło jest puste Empty: Hasło jest puste

View File

@ -328,6 +328,33 @@ Errors:
Invalid: 无效的用户数据 Invalid: 无效的用户数据
DomainNotAllowedAsUsername: 域名已存在,但无法使用 DomainNotAllowedAsUsername: 域名已存在,但无法使用
NotAllowedToLink: 不允许用户使用外部身份提供者注册 NotAllowedToLink: 不允许用户使用外部身份提供者注册
Profile:
NotFound: 未找到个人资料
NotChanged: 个人资料未更改
Empty: 简介是空的
FirstNameEmpty: 简介中的名字是空的
LastNameEmpty: 简介中的姓氏是空的
IDMissing: 简介ID丢失
Email:
NotFound: 电子邮件没有找到
Invalid: 电子邮件无效
AlreadyVerified: 电子邮件已经过验证
NotChanged: 电子邮件未更改
Empty: 电子邮件是空的
IDMissing: 电子邮件ID丢失
Phone:
NotFound: 手机号码未找到
Invalid: 手机号码无效
AlreadyVerified: 手机号码已经验证
Empty: 电话号码是空的
NotChanged: 电话号码没有改变
Address:
NotFound: 找不到地址
NotChanged: 地址没有改变
Username:
AlreadyExists: 用户名已被使用
Reserved: 用户名已被使用
Empty: 用户名是空的
Password: Password:
ConfirmationWrong: 密码不一致 ConfirmationWrong: 密码不一致
Empty: 密码为空 Empty: 密码为空

View File

@ -10,12 +10,12 @@ import (
) )
type Email struct { type Email struct {
Address string Address domain.EmailAddress
Verified bool Verified bool
} }
func (e *Email) Valid() bool { func (e *Email) Validate() error {
return e.Address != "" && domain.EmailRegex.MatchString(e.Address) return e.Address.Validate()
} }
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) { func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {

View File

@ -4,30 +4,16 @@ import (
"context" "context"
"time" "time"
"github.com/ttacon/libphonenumber"
"github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
) )
type Phone struct { type Phone struct {
Number string Number domain.PhoneNumber
Verified bool Verified bool
} }
func FormatPhoneNumber(number string) (string, error) {
if number == "" {
return "", nil
}
phoneNr, err := libphonenumber.Parse(number, libphonenumber.UNKNOWN_REGION)
if err != nil {
return "", errors.ThrowInvalidArgument(nil, "EVENT-so0wa", "Errors.User.Phone.Invalid")
}
number = libphonenumber.Format(phoneNr, libphonenumber.E164)
return number, nil
}
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) { func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg) return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
} }

View File

@ -3,12 +3,13 @@ package command
import ( import (
"testing" "testing"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
) )
func TestFormatPhoneNumber(t *testing.T) { func TestFormatPhoneNumber(t *testing.T) {
type args struct { type args struct {
number string number domain.PhoneNumber
} }
tests := []struct { tests := []struct {
name string name string
@ -44,10 +45,9 @@ func TestFormatPhoneNumber(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
formatted, err := FormatPhoneNumber(tt.args.number) normalized, err := tt.args.number.Normalize()
if tt.errFunc == nil && tt.result.Number != normalized {
if tt.errFunc == nil && tt.result.Number != formatted { t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.Number, normalized)
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.args.number, formatted)
} }
if tt.errFunc != nil && !tt.errFunc(err) { if tt.errFunc != nil && !tt.errFunc(err) {
t.Errorf("got wrong err: %v ", err) t.Errorf("got wrong err: %v ", err)

View File

@ -104,29 +104,31 @@ func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *Ad
type humanCreationCommand interface { type humanCreationCommand interface {
eventstore.Command eventstore.Command
AddPhoneData(phoneNumber string) AddPhoneData(phoneNumber domain.PhoneNumber)
AddPasswordData(secret *crypto.CryptoValue, changeRequired bool) AddPasswordData(secret *crypto.CryptoValue, changeRequired bool)
} }
func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm) preparation.Validation { func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) { return func() (_ preparation.CreateCommands, err error) {
if !human.Email.Valid() { if err := human.Email.Validate(); err != nil {
return nil, errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument") return nil, err
} }
if human.Username = strings.TrimSpace(human.Username); human.Username == "" { if human.Username = strings.TrimSpace(human.Username); human.Username == "" {
return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument") return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
} }
if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" { if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument") return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty")
} }
if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" { if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument") return nil, errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty")
} }
human.ensureDisplayName() human.ensureDisplayName()
if human.Phone.Number, err = FormatPhoneNumber(human.Phone.Number); err != nil { if human.Phone.Number != "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-tD6ax", "Errors.Invalid.Argument") if human.Phone.Number, err = human.Phone.Number.Normalize(); err != nil {
return nil, err
}
} }
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
@ -387,19 +389,12 @@ func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domai
return writeModelToHuman(registeredHuman), nil return writeModelToHuman(registeredHuman), nil
} }
func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) {
if orgID == "" || !human.IsValid() {
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-67Ms8", "Errors.User.Invalid")
}
if human.Password != nil && human.Password.SecretString != "" {
human.Password.ChangeRequired = true
}
return c.createHuman(ctx, orgID, human, nil, false, false, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
}
func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) {
if orgID == "" || !human.IsValid() { if orgID == "" {
return nil, nil, nil, "", errors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid") return nil, nil, nil, "", errors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty")
}
if err := human.Normalize(); err != nil {
return nil, nil, nil, "", err
} }
events, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) events, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
if err != nil { if err != nil {
@ -421,10 +416,16 @@ func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domai
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-JKefw", "Errors.User.Invalid") return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-JKefw", "Errors.User.Invalid")
} }
if human.Username = strings.TrimSpace(human.Username); human.Username == "" { if human.Username = strings.TrimSpace(human.Username); human.Username == "" {
human.Username = human.EmailAddress human.Username = string(human.EmailAddress)
} }
if orgID == "" || !human.IsValid() || link == nil && (human.Password == nil || human.Password.SecretString == "") { if orgID == "" {
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-9dk45", "Errors.User.Invalid") return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-hYsVH", "Errors.Org.Empty")
}
if err := human.Normalize(); err != nil {
return nil, nil, err
}
if link == nil && (human.Password == nil || human.Password.SecretString == "") {
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-X23na", "Errors.User.Password.Empty")
} }
if human.Password != nil && human.Password.SecretString != "" { if human.Password != nil && human.Password.SecretString != "" {
human.Password.ChangeRequired = false human.Password.ChangeRequired = false
@ -441,7 +442,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
return nil, nil, err return nil, nil, err
} }
human.Username = strings.TrimSpace(human.Username) human.Username = strings.TrimSpace(human.Username)
human.EmailAddress = strings.TrimSpace(human.EmailAddress) human.EmailAddress = human.EmailAddress.Normalize()
if !domainPolicy.UserLoginMustBeDomain { if !domainPolicy.UserLoginMustBeDomain {
index := strings.LastIndex(human.Username, "@") index := strings.LastIndex(human.Username, "@")
if index > 1 { if index > 1 {

View File

@ -13,8 +13,11 @@ import (
) )
func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, emailCodeGenerator crypto.Generator) (*domain.Email, error) { func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, emailCodeGenerator crypto.Generator) (*domain.Email, error) {
if !email.IsValid() || email.AggregateID == "" { if email.AggregateID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M9sf", "Errors.Email.Invalid") return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing")
}
if err := email.Validate(); err != nil {
return nil, err
} }
existingEmail, err := c.emailWriteModel(ctx, email.AggregateID, email.ResourceOwner) existingEmail, err := c.emailWriteModel(ctx, email.AggregateID, email.ResourceOwner)

View File

@ -14,7 +14,7 @@ import (
type HumanEmailWriteModel struct { type HumanEmailWriteModel struct {
eventstore.WriteModel eventstore.WriteModel
Email string Email domain.EmailAddress
IsEmailVerified bool IsEmailVerified bool
Code *crypto.CryptoValue Code *crypto.CryptoValue
@ -95,7 +95,7 @@ func (wm *HumanEmailWriteModel) Query() *eventstore.SearchQueryBuilder {
func (wm *HumanEmailWriteModel) NewChangedEvent( func (wm *HumanEmailWriteModel) NewChangedEvent(
ctx context.Context, ctx context.Context,
aggregate *eventstore.Aggregate, aggregate *eventstore.Aggregate,
email string, email domain.EmailAddress,
) (*user.HumanEmailChangedEvent, bool) { ) (*user.HumanEmailChangedEvent, bool) {
if wm.Email == email { if wm.Email == email {
return nil, false return nil, false

View File

@ -11,8 +11,8 @@ import (
"github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/repository/user"
) )
//ResendInitialMail resend inital mail and changes email if provided // ResendInitialMail resend initial mail and changes email if provided
func (c *Commands) ResendInitialMail(ctx context.Context, userID, email, resourceOwner string, initCodeGenerator crypto.Generator) (objectDetails *domain.ObjectDetails, err error) { func (c *Commands) ResendInitialMail(ctx context.Context, userID string, email domain.EmailAddress, resourceOwner string, initCodeGenerator crypto.Generator) (objectDetails *domain.ObjectDetails, err error) {
if userID == "" { if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing") return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing")
} }

View File

@ -14,7 +14,7 @@ import (
type HumanInitCodeWriteModel struct { type HumanInitCodeWriteModel struct {
eventstore.WriteModel eventstore.WriteModel
Email string Email domain.EmailAddress
IsEmailVerified bool IsEmailVerified bool
Code *crypto.CryptoValue Code *crypto.CryptoValue
@ -92,7 +92,7 @@ func (wm *HumanInitCodeWriteModel) Query() *eventstore.SearchQueryBuilder {
func (wm *HumanInitCodeWriteModel) NewChangedEvent( func (wm *HumanInitCodeWriteModel) NewChangedEvent(
ctx context.Context, ctx context.Context,
aggregate *eventstore.Aggregate, aggregate *eventstore.Aggregate,
email string, email domain.EmailAddress,
) (*user.HumanEmailChangedEvent, bool) { ) (*user.HumanEmailChangedEvent, bool) {
changedEvent := user.NewHumanEmailChangedEvent(ctx, aggregate, email) changedEvent := user.NewHumanEmailChangedEvent(ctx, aggregate, email)
return changedEvent, wm.Email != email return changedEvent, wm.Email != email

View File

@ -289,7 +289,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, eventstore: tt.fields.eventstore,
} }
got, err := r.ResendInitialMail(tt.args.ctx, tt.args.userID, tt.args.email, tt.args.resourceOwner, tt.args.secretGenerator) got, err := r.ResendInitialMail(tt.args.ctx, tt.args.userID, domain.EmailAddress(tt.args.email), tt.args.resourceOwner, tt.args.secretGenerator)
if tt.res.err == nil { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }

View File

@ -22,10 +22,10 @@ type HumanWriteModel struct {
Gender domain.Gender Gender domain.Gender
Avatar string Avatar string
Email string Email domain.EmailAddress
IsEmailVerified bool IsEmailVerified bool
Phone string Phone domain.PhoneNumber
IsPhoneVerified bool IsPhoneVerified bool
Country string Country string

View File

@ -69,7 +69,7 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
accountName := domain.GenerateLoginName(human.GetUsername(), org.PrimaryDomain, orgPolicy.UserLoginMustBeDomain) accountName := domain.GenerateLoginName(human.GetUsername(), org.PrimaryDomain, orgPolicy.UserLoginMustBeDomain)
if accountName == "" { if accountName == "" {
accountName = human.EmailAddress accountName = string(human.EmailAddress)
} }
key, secret, err := domain.NewOTPKey(c.multifactors.OTP.Issuer, accountName, c.multifactors.OTP.CryptoMFA) key, secret, err := domain.NewOTPKey(c.multifactors.OTP.Issuer, accountName, c.multifactors.OTP.CryptoMFA)
if err != nil { if err != nil {

View File

@ -14,10 +14,9 @@ import (
) )
func (c *Commands) ChangeHumanPhone(ctx context.Context, phone *domain.Phone, resourceOwner string, phoneCodeGenerator crypto.Generator) (*domain.Phone, error) { func (c *Commands) ChangeHumanPhone(ctx context.Context, phone *domain.Phone, resourceOwner string, phoneCodeGenerator crypto.Generator) (*domain.Phone, error) {
if !phone.IsValid() { if err := phone.Normalize(); err != nil {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-6M0ds", "Errors.Phone.Invalid") return nil, err
} }
existingPhone, err := c.phoneWriteModelByID(ctx, phone.AggregateID, resourceOwner) existingPhone, err := c.phoneWriteModelByID(ctx, phone.AggregateID, resourceOwner)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -14,7 +14,7 @@ import (
type HumanPhoneWriteModel struct { type HumanPhoneWriteModel struct {
eventstore.WriteModel eventstore.WriteModel
Phone string Phone domain.PhoneNumber
IsPhoneVerified bool IsPhoneVerified bool
Code *crypto.CryptoValue Code *crypto.CryptoValue
@ -107,7 +107,7 @@ func (wm *HumanPhoneWriteModel) Query() *eventstore.SearchQueryBuilder {
func (wm *HumanPhoneWriteModel) NewChangedEvent( func (wm *HumanPhoneWriteModel) NewChangedEvent(
ctx context.Context, ctx context.Context,
aggregate *eventstore.Aggregate, aggregate *eventstore.Aggregate,
phone string, phone domain.PhoneNumber,
) (*user.HumanPhoneChangedEvent, bool) { ) (*user.HumanPhoneChangedEvent, bool) {
changedEvent := user.NewHumanPhoneChangedEvent(ctx, aggregate, phone) changedEvent := user.NewHumanPhoneChangedEvent(ctx, aggregate, phone)
return changedEvent, phone != wm.Phone return changedEvent, phone != wm.Phone

View File

@ -9,10 +9,12 @@ import (
) )
func (c *Commands) ChangeHumanProfile(ctx context.Context, profile *domain.Profile) (*domain.Profile, error) { func (c *Commands) ChangeHumanProfile(ctx context.Context, profile *domain.Profile) (*domain.Profile, error) {
if !profile.IsValid() && profile.AggregateID != "" { if profile.AggregateID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8io0d", "Errors.User.Profile.Invalid") return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-AwbEB", "Errors.User.Profile.IDMissing")
}
if err := profile.Validate(); err != nil {
return nil, err
} }
existingProfile, err := c.profileWriteModelByID(ctx, profile.AggregateID, profile.ResourceOwner) existingProfile, err := c.profileWriteModelByID(ctx, profile.AggregateID, profile.ResourceOwner)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -3445,7 +3445,7 @@ func newAddHumanEvent(password string, changeRequired bool, phone string) *user.
changeRequired) changeRequired)
} }
if phone != "" { if phone != "" {
event.AddPhoneData(phone) event.AddPhoneData(domain.PhoneNumber(phone))
} }
return event return event
} }
@ -3473,7 +3473,7 @@ func newRegisterHumanEvent(username, password string, changeRequired bool, phone
changeRequired) changeRequired)
} }
if phone != "" { if phone != "" {
event.AddPhoneData(phone) event.AddPhoneData(domain.PhoneNumber(phone))
} }
return event return event
} }
@ -3503,7 +3503,7 @@ func TestAddHumanCommand(t *testing.T) {
}, },
}, },
want: Want{ want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument"), ValidationErr: errors.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"),
}, },
}, },
{ {
@ -3519,7 +3519,7 @@ func TestAddHumanCommand(t *testing.T) {
}, },
}, },
want: Want{ want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument"), ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"),
}, },
}, },
{ {
@ -3534,7 +3534,7 @@ func TestAddHumanCommand(t *testing.T) {
}, },
}, },
want: Want{ want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument"), ValidationErr: errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
}, },
}, },
{ {

View File

@ -155,7 +155,7 @@ func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner s
} }
accountName := domain.GenerateLoginName(user.GetUsername(), org.PrimaryDomain, orgPolicy.UserLoginMustBeDomain) accountName := domain.GenerateLoginName(user.GetUsername(), org.PrimaryDomain, orgPolicy.UserLoginMustBeDomain)
if accountName == "" { if accountName == "" {
accountName = user.EmailAddress accountName = string(user.EmailAddress)
} }
webAuthN, err := c.webauthnConfig.BeginRegistration(ctx, user, accountName, authenticatorPlatform, userVerification, isLoginUI, tokens...) webAuthN, err := c.webauthnConfig.BeginRegistration(ctx, user, accountName, authenticatorPlatform, userVerification, isLoginUI, tokens...)
if err != nil { if err != nil {

View File

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

View File

@ -4,6 +4,7 @@ import (
"time" "time"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/errors"
caos_errors "github.com/zitadel/zitadel/internal/errors" caos_errors "github.com/zitadel/zitadel/internal/errors"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
) )
@ -60,8 +61,22 @@ func (f Gender) Specified() bool {
return f > GenderUnspecified && f < genderCount return f > GenderUnspecified && f < genderCount
} }
func (u *Human) IsValid() bool { func (u *Human) Normalize() error {
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()) 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 { 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") return caos_errors.ThrowPreconditionFailed(nil, "DOMAIN-zSH7j", "Errors.Users.DomainPolicyNil")
} }
if !policy.UserLoginMustBeDomain && u.Profile != nil && u.Username == "" && u.Email != nil { if !policy.UserLoginMustBeDomain && u.Profile != nil && u.Username == "" && u.Email != nil {
u.Username = u.EmailAddress u.Username = string(u.EmailAddress)
} }
return nil return nil
} }

View File

@ -2,20 +2,38 @@ package domain
import ( import (
"regexp" "regexp"
"strings"
"time" "time"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/errors"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
) )
var ( 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 { type Email struct {
es_models.ObjectRoot es_models.ObjectRoot
EmailAddress string EmailAddress EmailAddress
IsEmailVerified bool IsEmailVerified bool
} }
@ -26,8 +44,11 @@ type EmailCode struct {
Expiry time.Duration Expiry time.Duration
} }
func (e *Email) IsValid() bool { func (e *Email) Validate() error {
return e.EmailAddress != "" && EmailRegex.MatchString(e.EmailAddress) if e == nil {
return errors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty")
}
return e.EmailAddress.Validate()
} }
func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, error) { func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, error) {

View File

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

View File

@ -4,19 +4,31 @@ import (
"time" "time"
"github.com/ttacon/libphonenumber" "github.com/ttacon/libphonenumber"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
caos_errs "github.com/zitadel/zitadel/internal/errors" caos_errs "github.com/zitadel/zitadel/internal/errors"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
) )
const ( const defaultRegion = "CH"
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 { type Phone struct {
es_models.ObjectRoot es_models.ObjectRoot
PhoneNumber string PhoneNumber PhoneNumber
IsPhoneVerified bool IsPhoneVerified bool
} }
@ -27,17 +39,16 @@ type PhoneCode struct {
Expiry time.Duration Expiry time.Duration
} }
func (p *Phone) IsValid() bool { func (p *Phone) Normalize() error {
err := p.formatPhone() if p == nil {
return p.PhoneNumber != "" && err == nil return caos_errs.ThrowInvalidArgument(nil, "PHONE-YlbwO", "Errors.User.Phone.Empty")
}
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")
} }
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 return nil
} }

View File

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

View File

@ -3,6 +3,7 @@ package domain
import ( import (
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/errors"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
) )
@ -19,8 +20,17 @@ type Profile struct {
LoginNames []string LoginNames []string
} }
func (p *Profile) IsValid() bool { func (p *Profile) Validate() error {
return p.FirstName != "" && p.LastName != "" 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 { func AvatarURL(prefix, resourceOwner, key string) string {

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
) )
// Provider is the minimal implementation for a 3rd party authentication provider // Provider is the minimal implementation for a 3rd party authentication provider
@ -24,9 +26,9 @@ type User interface {
GetDisplayName() string GetDisplayName() string
GetNickname() string GetNickname() string
GetPreferredUsername() string GetPreferredUsername() string
GetEmail() string GetEmail() domain.EmailAddress
IsEmailVerified() bool IsEmailVerified() bool
GetPhone() string GetPhone() domain.PhoneNumber
IsPhoneVerified() bool IsPhoneVerified() bool
GetPreferredLanguage() language.Tag GetPreferredLanguage() language.Tag
GetAvatarURL() string GetAvatarURL() string

View File

@ -7,6 +7,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth" "github.com/zitadel/zitadel/internal/idp/providers/oauth"
) )
@ -120,13 +121,13 @@ func newConfig(tenant TenantType, clientID, secret, callbackURL string, scopes [
// AzureAD does not return an `email_verified` claim. // AzureAD does not return an `email_verified` claim.
// The verification can be automatically activated on the provider ([WithEmailVerified]) // The verification can be automatically activated on the provider ([WithEmailVerified])
type User struct { type User struct {
Sub string `json:"sub"` Sub string `json:"sub"`
FamilyName string `json:"family_name"` FamilyName string `json:"family_name"`
GivenName string `json:"given_name"` GivenName string `json:"given_name"`
Name string `json:"name"` Name string `json:"name"`
PreferredUsername string `json:"preferred_username"` PreferredUsername string `json:"preferred_username"`
Email string `json:"email"` Email domain.EmailAddress `json:"email"`
Picture string `json:"picture"` Picture string `json:"picture"`
isEmailVerified bool isEmailVerified bool
} }
@ -162,7 +163,7 @@ func (u *User) GetPreferredUsername() string {
} }
// GetEmail is an implementation of the [idp.User] interface. // GetEmail is an implementation of the [idp.User] interface.
func (u *User) GetEmail() string { func (u *User) GetEmail() domain.EmailAddress {
return u.Email return u.Email
} }
@ -176,7 +177,7 @@ func (u *User) IsEmailVerified() bool {
// GetPhone is an implementation of the [idp.User] interface. // GetPhone is an implementation of the [idp.User] interface.
// It returns an empty string because AzureAD does not provide the user's phone. // It returns an empty string because AzureAD does not provide the user's phone.
func (u *User) GetPhone() string { func (u *User) GetPhone() domain.PhoneNumber {
return "" return ""
} }

View File

@ -14,6 +14,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth" "github.com/zitadel/zitadel/internal/idp/providers/oauth"
) )
@ -259,9 +260,9 @@ func TestSession_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName()) a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname()) a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername()) a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail()) a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified()) a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone()) a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL()) a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@ -4,6 +4,8 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/zitadel/zitadel/internal/domain"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/language" "golang.org/x/text/language"
@ -68,44 +70,44 @@ func newConfig(clientID, secret, callbackURL, authURL, tokenURL string, scopes [
// User is a representation of the authenticated GitHub user and implements the [idp.User] interface // User is a representation of the authenticated GitHub user and implements the [idp.User] interface
// https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user // https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user
type User struct { type User struct {
Login string `json:"login"` Login string `json:"login"`
ID int `json:"id"` ID int `json:"id"`
NodeId string `json:"node_id"` NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"` AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"` GravatarId string `json:"gravatar_id"`
Url string `json:"url"` Url string `json:"url"`
HtmlUrl string `json:"html_url"` HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"` FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"` FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"` GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"` StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"` SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"` OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"` ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"` EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"` ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"` Type string `json:"type"`
SiteAdmin bool `json:"site_admin"` SiteAdmin bool `json:"site_admin"`
Name string `json:"name"` Name string `json:"name"`
Company string `json:"company"` Company string `json:"company"`
Blog string `json:"blog"` Blog string `json:"blog"`
Location string `json:"location"` Location string `json:"location"`
Email string `json:"email"` Email domain.EmailAddress `json:"email"`
Hireable bool `json:"hireable"` Hireable bool `json:"hireable"`
Bio string `json:"bio"` Bio string `json:"bio"`
TwitterUsername string `json:"twitter_username"` TwitterUsername string `json:"twitter_username"`
PublicRepos int `json:"public_repos"` PublicRepos int `json:"public_repos"`
PublicGists int `json:"public_gists"` PublicGists int `json:"public_gists"`
Followers int `json:"followers"` Followers int `json:"followers"`
Following int `json:"following"` Following int `json:"following"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
PrivateGists int `json:"private_gists"` PrivateGists int `json:"private_gists"`
TotalPrivateRepos int `json:"total_private_repos"` TotalPrivateRepos int `json:"total_private_repos"`
OwnedPrivateRepos int `json:"owned_private_repos"` OwnedPrivateRepos int `json:"owned_private_repos"`
DiskUsage int `json:"disk_usage"` DiskUsage int `json:"disk_usage"`
Collaborators int `json:"collaborators"` Collaborators int `json:"collaborators"`
TwoFactorAuthentication bool `json:"two_factor_authentication"` TwoFactorAuthentication bool `json:"two_factor_authentication"`
Plan struct { Plan struct {
Name string `json:"name"` Name string `json:"name"`
Space int `json:"space"` Space int `json:"space"`
@ -150,7 +152,7 @@ func (u *User) GetPreferredUsername() string {
} }
// GetEmail is an implementation of the [idp.User] interface. // GetEmail is an implementation of the [idp.User] interface.
func (u *User) GetEmail() string { func (u *User) GetEmail() domain.EmailAddress {
return u.Email return u.Email
} }
@ -162,7 +164,7 @@ func (u *User) IsEmailVerified() bool {
// GetPhone is an implementation of the [idp.User] interface. // GetPhone is an implementation of the [idp.User] interface.
// It returns an empty string because GitHub does not provide the user's phone. // It returns an empty string because GitHub does not provide the user's phone.
func (u *User) GetPhone() string { func (u *User) GetPhone() domain.PhoneNumber {
return "" return ""
} }

View File

@ -14,6 +14,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth" "github.com/zitadel/zitadel/internal/idp/providers/oauth"
) )
@ -188,9 +189,9 @@ func TestSession_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName()) a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname()) a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername()) a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail()) a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified()) a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone()) a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL()) a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@ -14,6 +14,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp/providers/oidc" "github.com/zitadel/zitadel/internal/idp/providers/oidc"
) )
@ -187,9 +188,9 @@ func TestProvider_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName()) a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname()) a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername()) a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail()) a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified()) a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone()) a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL()) a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@ -43,5 +43,5 @@ type User struct {
// GetPreferredUsername implements the [idp.User] interface. // GetPreferredUsername implements the [idp.User] interface.
// It returns the email, because Google does not return a username. // It returns the email, because Google does not return a username.
func (u *User) GetPreferredUsername() string { func (u *User) GetPreferredUsername() string {
return u.GetEmail() return string(u.GetEmail())
} }

View File

@ -14,6 +14,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp/providers/oidc" "github.com/zitadel/zitadel/internal/idp/providers/oidc"
) )
@ -188,9 +189,9 @@ func TestSession_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName()) a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname()) a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername()) a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail()) a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified()) a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone()) a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL()) a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@ -12,6 +12,7 @@ import (
"github.com/zitadel/oidc/v2/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
) )
@ -106,8 +107,8 @@ func (u *User) GetNickname() string {
return u.IDTokenClaims.GetNickname() return u.IDTokenClaims.GetNickname()
} }
func (u *User) GetPhone() string { func (u *User) GetPhone() domain.PhoneNumber {
return u.IDTokenClaims.GetPhoneNumber() return domain.PhoneNumber(u.IDTokenClaims.GetPhoneNumber())
} }
func (u *User) IsPhoneVerified() bool { func (u *User) IsPhoneVerified() bool {
@ -121,3 +122,7 @@ func (u *User) GetPreferredLanguage() language.Tag {
func (u *User) GetAvatarURL() string { func (u *User) GetAvatarURL() string {
return u.IDTokenClaims.GetPicture() return u.IDTokenClaims.GetPicture()
} }
func (u *User) GetEmail() domain.EmailAddress {
return domain.EmailAddress(u.IDTokenClaims.GetEmail())
}

View File

@ -17,6 +17,7 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
) )
func TestSession_FetchUser(t *testing.T) { func TestSession_FetchUser(t *testing.T) {
@ -193,9 +194,9 @@ func TestSession_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName()) a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname()) a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername()) a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail()) a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified()) a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone()) a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL()) a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@ -10,6 +10,7 @@ import (
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
) )
@ -87,9 +88,9 @@ func (s *Session) FetchUser(_ context.Context) (idp.User, error) {
user.GetAttributeValue(s.Provider.displayNameAttribute), user.GetAttributeValue(s.Provider.displayNameAttribute),
user.GetAttributeValue(s.Provider.nickNameAttribute), user.GetAttributeValue(s.Provider.nickNameAttribute),
user.GetAttributeValue(s.Provider.preferredUsernameAttribute), user.GetAttributeValue(s.Provider.preferredUsernameAttribute),
user.GetAttributeValue(s.Provider.emailAttribute), domain.EmailAddress(user.GetAttributeValue(s.Provider.emailAttribute)),
emailVerified, emailVerified,
user.GetAttributeValue(s.Provider.phoneAttribute), domain.PhoneNumber(user.GetAttributeValue(s.Provider.phoneAttribute)),
phoneVerified, phoneVerified,
language.Make(user.GetAttributeValue(s.Provider.preferredLanguageAttribute)), language.Make(user.GetAttributeValue(s.Provider.preferredLanguageAttribute)),
user.GetAttributeValue(s.Provider.avatarURLAttribute), user.GetAttributeValue(s.Provider.avatarURLAttribute),

View File

@ -1,6 +1,10 @@
package ldap package ldap
import "golang.org/x/text/language" import (
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
)
type User struct { type User struct {
id string id string
@ -9,9 +13,9 @@ type User struct {
displayName string displayName string
nickName string nickName string
preferredUsername string preferredUsername string
email string email domain.EmailAddress
emailVerified bool emailVerified bool
phone string phone domain.PhoneNumber
phoneVerified bool phoneVerified bool
preferredLanguage language.Tag preferredLanguage language.Tag
avatarURL string avatarURL string
@ -25,9 +29,9 @@ func NewUser(
displayName string, displayName string,
nickName string, nickName string,
preferredUsername string, preferredUsername string,
email string, email domain.EmailAddress,
emailVerified bool, emailVerified bool,
phone string, phone domain.PhoneNumber,
phoneVerified bool, phoneVerified bool,
preferredLanguage language.Tag, preferredLanguage language.Tag,
avatarURL string, avatarURL string,
@ -68,13 +72,13 @@ func (u *User) GetNickname() string {
func (u *User) GetPreferredUsername() string { func (u *User) GetPreferredUsername() string {
return u.preferredUsername return u.preferredUsername
} }
func (u *User) GetEmail() string { func (u *User) GetEmail() domain.EmailAddress {
return u.email return u.email
} }
func (u *User) IsEmailVerified() bool { func (u *User) IsEmailVerified() bool {
return u.emailVerified return u.emailVerified
} }
func (u *User) GetPhone() string { func (u *User) GetPhone() domain.PhoneNumber {
return u.phone return u.phone
} }
func (u *User) IsPhoneVerified() bool { func (u *User) IsPhoneVerified() bool {

View File

@ -7,6 +7,7 @@ import (
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
) )
@ -74,7 +75,7 @@ func (u *UserMapper) GetPreferredUsername() string {
} }
// GetEmail is an implementation of the [idp.User] interface. // GetEmail is an implementation of the [idp.User] interface.
func (u *UserMapper) GetEmail() string { func (u *UserMapper) GetEmail() domain.EmailAddress {
return "" return ""
} }
@ -84,7 +85,7 @@ func (u *UserMapper) IsEmailVerified() bool {
} }
// GetPhone is an implementation of the [idp.User] interface. // GetPhone is an implementation of the [idp.User] interface.
func (u *UserMapper) GetPhone() string { func (u *UserMapper) GetPhone() domain.PhoneNumber {
return "" return ""
} }

View File

@ -13,6 +13,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
) )
@ -260,9 +261,9 @@ func TestProvider_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName()) a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname()) a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername()) a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail()) a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified()) a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone()) a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL()) a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@ -8,6 +8,7 @@ import (
"github.com/zitadel/oidc/v2/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
) )
@ -82,8 +83,8 @@ func (u *User) GetDisplayName() string {
return u.GetName() return u.GetName()
} }
func (u *User) GetPhone() string { func (u *User) GetPhone() domain.PhoneNumber {
return u.GetPhoneNumber() return domain.PhoneNumber(u.GetPhoneNumber())
} }
func (u *User) IsPhoneVerified() bool { func (u *User) IsPhoneVerified() bool {
@ -97,3 +98,7 @@ func (u *User) GetPreferredLanguage() language.Tag {
func (u *User) GetAvatarURL() string { func (u *User) GetAvatarURL() string {
return u.GetPicture() return u.GetPicture()
} }
func (u *User) GetEmail() domain.EmailAddress {
return domain.EmailAddress(u.UserInfo.GetEmail())
}

View File

@ -17,6 +17,7 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
) )
@ -281,9 +282,9 @@ func TestSession_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName()) a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname()) a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername()) a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail()) a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified()) a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone()) a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL()) a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@ -342,7 +342,7 @@ func (p *userProjection) reduceHumanAdded(event eventstore.Event) (*handler.Stat
handler.NewCol(HumanPreferredLanguageCol, &sql.NullString{String: e.PreferredLanguage.String(), Valid: !e.PreferredLanguage.IsRoot()}), handler.NewCol(HumanPreferredLanguageCol, &sql.NullString{String: e.PreferredLanguage.String(), Valid: !e.PreferredLanguage.IsRoot()}),
handler.NewCol(HumanGenderCol, &sql.NullInt16{Int16: int16(e.Gender), Valid: e.Gender.Specified()}), handler.NewCol(HumanGenderCol, &sql.NullInt16{Int16: int16(e.Gender), Valid: e.Gender.Specified()}),
handler.NewCol(HumanEmailCol, e.EmailAddress), handler.NewCol(HumanEmailCol, e.EmailAddress),
handler.NewCol(HumanPhoneCol, &sql.NullString{String: e.PhoneNumber, Valid: e.PhoneNumber != ""}), handler.NewCol(HumanPhoneCol, &sql.NullString{String: string(e.PhoneNumber), Valid: e.PhoneNumber != ""}),
}, },
crdb.WithTableSuffix(UserHumanSuffix), crdb.WithTableSuffix(UserHumanSuffix),
), ),
@ -351,7 +351,7 @@ func (p *userProjection) reduceHumanAdded(event eventstore.Event) (*handler.Stat
handler.NewCol(NotifyUserIDCol, e.Aggregate().ID), handler.NewCol(NotifyUserIDCol, e.Aggregate().ID),
handler.NewCol(NotifyInstanceIDCol, e.Aggregate().InstanceID), handler.NewCol(NotifyInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCol(NotifyLastEmailCol, e.EmailAddress), handler.NewCol(NotifyLastEmailCol, e.EmailAddress),
handler.NewCol(NotifyLastPhoneCol, &sql.NullString{String: e.PhoneNumber, Valid: e.PhoneNumber != ""}), handler.NewCol(NotifyLastPhoneCol, &sql.NullString{String: string(e.PhoneNumber), Valid: e.PhoneNumber != ""}),
handler.NewCol(NotifyPasswordSetCol, e.Secret != nil), handler.NewCol(NotifyPasswordSetCol, e.Secret != nil),
}, },
crdb.WithTableSuffix(UserNotifySuffix), crdb.WithTableSuffix(UserNotifySuffix),
@ -390,7 +390,7 @@ func (p *userProjection) reduceHumanRegistered(event eventstore.Event) (*handler
handler.NewCol(HumanPreferredLanguageCol, &sql.NullString{String: e.PreferredLanguage.String(), Valid: !e.PreferredLanguage.IsRoot()}), handler.NewCol(HumanPreferredLanguageCol, &sql.NullString{String: e.PreferredLanguage.String(), Valid: !e.PreferredLanguage.IsRoot()}),
handler.NewCol(HumanGenderCol, &sql.NullInt16{Int16: int16(e.Gender), Valid: e.Gender.Specified()}), handler.NewCol(HumanGenderCol, &sql.NullInt16{Int16: int16(e.Gender), Valid: e.Gender.Specified()}),
handler.NewCol(HumanEmailCol, e.EmailAddress), handler.NewCol(HumanEmailCol, e.EmailAddress),
handler.NewCol(HumanPhoneCol, &sql.NullString{String: e.PhoneNumber, Valid: e.PhoneNumber != ""}), handler.NewCol(HumanPhoneCol, &sql.NullString{String: string(e.PhoneNumber), Valid: e.PhoneNumber != ""}),
}, },
crdb.WithTableSuffix(UserHumanSuffix), crdb.WithTableSuffix(UserHumanSuffix),
), ),
@ -399,7 +399,7 @@ func (p *userProjection) reduceHumanRegistered(event eventstore.Event) (*handler
handler.NewCol(NotifyUserIDCol, e.Aggregate().ID), handler.NewCol(NotifyUserIDCol, e.Aggregate().ID),
handler.NewCol(NotifyInstanceIDCol, e.Aggregate().InstanceID), handler.NewCol(NotifyInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCol(NotifyLastEmailCol, e.EmailAddress), handler.NewCol(NotifyLastEmailCol, e.EmailAddress),
handler.NewCol(NotifyLastPhoneCol, &sql.NullString{String: e.PhoneNumber, Valid: e.PhoneNumber != ""}), handler.NewCol(NotifyLastPhoneCol, &sql.NullString{String: string(e.PhoneNumber), Valid: e.PhoneNumber != ""}),
handler.NewCol(NotifyPasswordSetCol, e.Secret != nil), handler.NewCol(NotifyPasswordSetCol, e.Secret != nil),
}, },
crdb.WithTableSuffix(UserNotifySuffix), crdb.WithTableSuffix(UserNotifySuffix),
@ -660,7 +660,7 @@ func (p *userProjection) reduceHumanPhoneChanged(event eventstore.Event) (*handl
), ),
crdb.AddUpdateStatement( crdb.AddUpdateStatement(
[]handler.Column{ []handler.Column{
handler.NewCol(NotifyLastPhoneCol, &sql.NullString{String: e.PhoneNumber, Valid: e.PhoneNumber != ""}), handler.NewCol(NotifyLastPhoneCol, &sql.NullString{String: string(e.PhoneNumber), Valid: e.PhoneNumber != ""}),
}, },
[]handler.Condition{ []handler.Condition{
handler.NewCond(NotifyUserIDCol, e.Aggregate().ID), handler.NewCond(NotifyUserIDCol, e.Aggregate().ID),
@ -786,7 +786,7 @@ func (p *userProjection) reduceHumanEmailChanged(event eventstore.Event) (*handl
), ),
crdb.AddUpdateStatement( crdb.AddUpdateStatement(
[]handler.Column{ []handler.Column{
handler.NewCol(NotifyLastEmailCol, &sql.NullString{String: e.EmailAddress, Valid: e.EmailAddress != ""}), handler.NewCol(NotifyLastEmailCol, &sql.NullString{String: string(e.EmailAddress), Valid: e.EmailAddress != ""}),
}, },
[]handler.Condition{ []handler.Condition{
handler.NewCond(NotifyUserIDCol, e.Aggregate().ID), handler.NewCond(NotifyUserIDCol, e.Aggregate().ID),

View File

@ -75,7 +75,7 @@ func TestUserProjection_reduces(t *testing.T) {
&sql.NullString{String: "display-name", Valid: true}, &sql.NullString{String: "display-name", Valid: true},
&sql.NullString{String: "ch-DE", Valid: true}, &sql.NullString{String: "ch-DE", Valid: true},
&sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true}, &sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true},
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{String: "+41 00 000 00 00", Valid: true}, &sql.NullString{String: "+41 00 000 00 00", Valid: true},
}, },
}, },
@ -84,7 +84,7 @@ func TestUserProjection_reduces(t *testing.T) {
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{String: "+41 00 000 00 00", Valid: true}, &sql.NullString{String: "+41 00 000 00 00", Valid: true},
false, false,
}, },
@ -144,7 +144,7 @@ func TestUserProjection_reduces(t *testing.T) {
&sql.NullString{String: "display-name", Valid: true}, &sql.NullString{String: "display-name", Valid: true},
&sql.NullString{String: "ch-DE", Valid: true}, &sql.NullString{String: "ch-DE", Valid: true},
&sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true}, &sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true},
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{String: "+41 00 000 00 00", Valid: true}, &sql.NullString{String: "+41 00 000 00 00", Valid: true},
}, },
}, },
@ -153,7 +153,7 @@ func TestUserProjection_reduces(t *testing.T) {
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{String: "+41 00 000 00 00", Valid: true}, &sql.NullString{String: "+41 00 000 00 00", Valid: true},
false, false,
}, },
@ -208,7 +208,7 @@ func TestUserProjection_reduces(t *testing.T) {
&sql.NullString{}, &sql.NullString{},
&sql.NullString{String: "und", Valid: false}, &sql.NullString{String: "und", Valid: false},
&sql.NullInt16{}, &sql.NullInt16{},
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{}, &sql.NullString{},
}, },
}, },
@ -217,7 +217,7 @@ func TestUserProjection_reduces(t *testing.T) {
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{String: "", Valid: false}, &sql.NullString{String: "", Valid: false},
false, false,
}, },
@ -277,7 +277,7 @@ func TestUserProjection_reduces(t *testing.T) {
&sql.NullString{String: "display-name", Valid: true}, &sql.NullString{String: "display-name", Valid: true},
&sql.NullString{String: "ch-DE", Valid: true}, &sql.NullString{String: "ch-DE", Valid: true},
&sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true}, &sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true},
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{String: "+41 00 000 00 00", Valid: true}, &sql.NullString{String: "+41 00 000 00 00", Valid: true},
}, },
}, },
@ -286,7 +286,7 @@ func TestUserProjection_reduces(t *testing.T) {
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{String: "+41 00 000 00 00", Valid: true}, &sql.NullString{String: "+41 00 000 00 00", Valid: true},
false, false,
}, },
@ -346,7 +346,7 @@ func TestUserProjection_reduces(t *testing.T) {
&sql.NullString{String: "display-name", Valid: true}, &sql.NullString{String: "display-name", Valid: true},
&sql.NullString{String: "ch-DE", Valid: true}, &sql.NullString{String: "ch-DE", Valid: true},
&sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true}, &sql.NullInt16{Int16: int16(domain.GenderFemale), Valid: true},
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{String: "+41 00 000 00 00", Valid: true}, &sql.NullString{String: "+41 00 000 00 00", Valid: true},
}, },
}, },
@ -355,7 +355,7 @@ func TestUserProjection_reduces(t *testing.T) {
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{String: "+41 00 000 00 00", Valid: true}, &sql.NullString{String: "+41 00 000 00 00", Valid: true},
false, false,
}, },
@ -410,7 +410,7 @@ func TestUserProjection_reduces(t *testing.T) {
&sql.NullString{}, &sql.NullString{},
&sql.NullString{String: "und", Valid: false}, &sql.NullString{String: "und", Valid: false},
&sql.NullInt16{}, &sql.NullInt16{},
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{}, &sql.NullString{},
}, },
}, },
@ -419,7 +419,7 @@ func TestUserProjection_reduces(t *testing.T) {
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
&sql.NullString{String: "", Valid: false}, &sql.NullString{String: "", Valid: false},
false, false,
}, },
@ -879,7 +879,7 @@ func TestUserProjection_reduces(t *testing.T) {
{ {
expectedStmt: "UPDATE projections.users8_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedStmt: "UPDATE projections.users8_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"+41 00 000 00 00", domain.PhoneNumber("+41 00 000 00 00"),
false, false,
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -927,7 +927,7 @@ func TestUserProjection_reduces(t *testing.T) {
{ {
expectedStmt: "UPDATE projections.users8_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedStmt: "UPDATE projections.users8_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"+41 00 000 00 00", domain.PhoneNumber("+41 00 000 00 00"),
false, false,
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -1157,7 +1157,7 @@ func TestUserProjection_reduces(t *testing.T) {
{ {
expectedStmt: "UPDATE projections.users8_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedStmt: "UPDATE projections.users8_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
false, false,
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -1205,7 +1205,7 @@ func TestUserProjection_reduces(t *testing.T) {
{ {
expectedStmt: "UPDATE projections.users8_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedStmt: "UPDATE projections.users8_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"email@zitadel.com", domain.EmailAddress("email@zitadel.com"),
false, false,
"agg-id", "agg-id",
"instance-id", "instance-id",

View File

@ -47,9 +47,9 @@ type Human struct {
AvatarKey string AvatarKey string
PreferredLanguage language.Tag PreferredLanguage language.Tag
Gender domain.Gender Gender domain.Gender
Email string Email domain.EmailAddress
IsEmailVerified bool IsEmailVerified bool
Phone string Phone domain.PhoneNumber
IsPhoneVerified bool IsPhoneVerified bool
} }
@ -74,7 +74,7 @@ type Email struct {
ChangeDate time.Time ChangeDate time.Time
ResourceOwner string ResourceOwner string
Sequence uint64 Sequence uint64
Email string Email domain.EmailAddress
IsVerified bool IsVerified bool
} }
@ -847,9 +847,9 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder
AvatarKey: avatarKey.String, AvatarKey: avatarKey.String,
PreferredLanguage: language.Make(preferredLanguage.String), PreferredLanguage: language.Make(preferredLanguage.String),
Gender: domain.Gender(gender.Int32), Gender: domain.Gender(gender.Int32),
Email: email.String, Email: domain.EmailAddress(email.String),
IsEmailVerified: isEmailVerified.Bool, IsEmailVerified: isEmailVerified.Bool,
Phone: phone.String, Phone: domain.PhoneNumber(phone.String),
IsPhoneVerified: isPhoneVerified.Bool, IsPhoneVerified: isPhoneVerified.Bool,
} }
} else if machineID.Valid { } else if machineID.Valid {
@ -970,7 +970,7 @@ func prepareEmailQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde
return nil, errors.ThrowPreconditionFailed(nil, "QUERY-pt7HY", "Errors.User.NotHuman") return nil, errors.ThrowPreconditionFailed(nil, "QUERY-pt7HY", "Errors.User.NotHuman")
} }
e.Email = email.String e.Email = domain.EmailAddress(email.String)
e.IsVerified = isEmailVerified.Bool e.IsVerified = isEmailVerified.Bool
return e, nil return e, nil
@ -1318,9 +1318,9 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde
AvatarKey: avatarKey.String, AvatarKey: avatarKey.String,
PreferredLanguage: language.Make(preferredLanguage.String), PreferredLanguage: language.Make(preferredLanguage.String),
Gender: domain.Gender(gender.Int32), Gender: domain.Gender(gender.Int32),
Email: email.String, Email: domain.EmailAddress(email.String),
IsEmailVerified: isEmailVerified.Bool, IsEmailVerified: isEmailVerified.Bool,
Phone: phone.String, Phone: domain.PhoneNumber(phone.String),
IsPhoneVerified: isPhoneVerified.Bool, IsPhoneVerified: isPhoneVerified.Bool,
} }
} else if machineID.Valid { } else if machineID.Valid {

View File

@ -38,9 +38,9 @@ type HumanAddedEvent struct {
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"` PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
Gender domain.Gender `json:"gender,omitempty"` Gender domain.Gender `json:"gender,omitempty"`
EmailAddress string `json:"email,omitempty"` EmailAddress domain.EmailAddress `json:"email,omitempty"`
PhoneNumber string `json:"phone,omitempty"` PhoneNumber domain.PhoneNumber `json:"phone,omitempty"`
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
Locality string `json:"locality,omitempty"` Locality string `json:"locality,omitempty"`
@ -75,7 +75,7 @@ func (e *HumanAddedEvent) AddAddressData(
} }
func (e *HumanAddedEvent) AddPhoneData( func (e *HumanAddedEvent) AddPhoneData(
phoneNumber string, phoneNumber domain.PhoneNumber,
) { ) {
e.PhoneNumber = phoneNumber e.PhoneNumber = phoneNumber
} }
@ -99,7 +99,7 @@ func NewHumanAddedEvent(
displayName string, displayName string,
preferredLanguage language.Tag, preferredLanguage language.Tag,
gender domain.Gender, gender domain.Gender,
emailAddress string, emailAddress domain.EmailAddress,
userLoginMustBeDomain bool, userLoginMustBeDomain bool,
) *HumanAddedEvent { ) *HumanAddedEvent {
return &HumanAddedEvent{ return &HumanAddedEvent{
@ -133,30 +133,24 @@ func HumanAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
} }
type HumanRegisteredEvent struct { type HumanRegisteredEvent struct {
eventstore.BaseEvent `json:"-"` eventstore.BaseEvent `json:"-"`
UserName string `json:"userName"` UserName string `json:"userName"`
userLoginMustBeDomain bool userLoginMustBeDomain bool
FirstName string `json:"firstName,omitempty"`
FirstName string `json:"firstName,omitempty"` LastName string `json:"lastName,omitempty"`
LastName string `json:"lastName,omitempty"` NickName string `json:"nickName,omitempty"`
NickName string `json:"nickName,omitempty"` DisplayName string `json:"displayName,omitempty"`
DisplayName string `json:"displayName,omitempty"` PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"` Gender domain.Gender `json:"gender,omitempty"`
Gender domain.Gender `json:"gender,omitempty"` EmailAddress domain.EmailAddress `json:"email,omitempty"`
PhoneNumber domain.PhoneNumber `json:"phone,omitempty"`
EmailAddress string `json:"email,omitempty"` Country string `json:"country,omitempty"`
Locality string `json:"locality,omitempty"`
PhoneNumber string `json:"phone,omitempty"` PostalCode string `json:"postalCode,omitempty"`
Region string `json:"region,omitempty"`
Country string `json:"country,omitempty"` StreetAddress string `json:"streetAddress,omitempty"`
Locality string `json:"locality,omitempty"` Secret *crypto.CryptoValue `json:"secret,omitempty"`
PostalCode string `json:"postalCode,omitempty"` ChangeRequired bool `json:"changeRequired,omitempty"`
Region string `json:"region,omitempty"`
StreetAddress string `json:"streetAddress,omitempty"`
Secret *crypto.CryptoValue `json:"secret,omitempty"`
ChangeRequired bool `json:"changeRequired,omitempty"`
} }
func (e *HumanRegisteredEvent) Data() interface{} { func (e *HumanRegisteredEvent) Data() interface{} {
@ -182,7 +176,7 @@ func (e *HumanRegisteredEvent) AddAddressData(
} }
func (e *HumanRegisteredEvent) AddPhoneData( func (e *HumanRegisteredEvent) AddPhoneData(
phoneNumber string, phoneNumber domain.PhoneNumber,
) { ) {
e.PhoneNumber = phoneNumber e.PhoneNumber = phoneNumber
} }
@ -206,7 +200,7 @@ func NewHumanRegisteredEvent(
displayName string, displayName string,
preferredLanguage language.Tag, preferredLanguage language.Tag,
gender domain.Gender, gender domain.Gender,
emailAddress string, emailAddress domain.EmailAddress,
userLoginMustBeDomain bool, userLoginMustBeDomain bool,
) *HumanRegisteredEvent { ) *HumanRegisteredEvent {
return &HumanRegisteredEvent{ return &HumanRegisteredEvent{

View File

@ -5,10 +5,10 @@ import (
"encoding/json" "encoding/json"
"time" "time"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository" "github.com/zitadel/zitadel/internal/eventstore/repository"
) )
@ -24,7 +24,7 @@ const (
type HumanEmailChangedEvent struct { type HumanEmailChangedEvent struct {
eventstore.BaseEvent `json:"-"` eventstore.BaseEvent `json:"-"`
EmailAddress string `json:"email,omitempty"` EmailAddress domain.EmailAddress `json:"email,omitempty"`
} }
func (e *HumanEmailChangedEvent) Data() interface{} { func (e *HumanEmailChangedEvent) Data() interface{} {
@ -35,7 +35,7 @@ func (e *HumanEmailChangedEvent) UniqueConstraints() []*eventstore.EventUniqueCo
return nil return nil
} }
func NewHumanEmailChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, emailAddress string) *HumanEmailChangedEvent { func NewHumanEmailChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, emailAddress domain.EmailAddress) *HumanEmailChangedEvent {
return &HumanEmailChangedEvent{ return &HumanEmailChangedEvent{
BaseEvent: *eventstore.NewBaseEventForPush( BaseEvent: *eventstore.NewBaseEventForPush(
ctx, ctx,

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository" "github.com/zitadel/zitadel/internal/eventstore/repository"
@ -24,7 +25,7 @@ const (
type HumanPhoneChangedEvent struct { type HumanPhoneChangedEvent struct {
eventstore.BaseEvent `json:"-"` eventstore.BaseEvent `json:"-"`
PhoneNumber string `json:"phone,omitempty"` PhoneNumber domain.PhoneNumber `json:"phone,omitempty"`
} }
func (e *HumanPhoneChangedEvent) Data() interface{} { func (e *HumanPhoneChangedEvent) Data() interface{} {
@ -35,7 +36,7 @@ func (e *HumanPhoneChangedEvent) UniqueConstraints() []*eventstore.EventUniqueCo
return nil return nil
} }
func NewHumanPhoneChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, phone string) *HumanPhoneChangedEvent { func NewHumanPhoneChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, phone domain.PhoneNumber) *HumanPhoneChangedEvent {
return &HumanPhoneChangedEvent{ return &HumanPhoneChangedEvent{
BaseEvent: *eventstore.NewBaseEventForPush( BaseEvent: *eventstore.NewBaseEventForPush(
ctx, ctx,

View File

@ -69,17 +69,24 @@ Errors:
UsernameNotChanged: Benutzername wurde nicht verändert UsernameNotChanged: Benutzername wurde nicht verändert
Profile: Profile:
NotFound: Profil nicht gefunden NotFound: Profil nicht gefunden
NotChanged: Profile nicht verändert NotChanged: Profil nicht verändert
Invalid: Profildaten sind ungültig Empty: Profil ist leer
FirstNameEmpty: Vorname im Profil ist leer
LastNameEmpty: Nachname im Profil ist leer
IDMissing: Profil ID fehlt
Email: Email:
NotFound: Email nicht gefunden NotFound: Email nicht gefunden
Invalid: Email ist ungültig Invalid: Email ist ungültig
AlreadyVerified: Email ist bereits verifiziert AlreadyVerified: Email ist bereits verifiziert
NotChanged: Email wurde nicht geändert NotChanged: Email wurde nicht geändert
Empty: Email ist leer
IDMissing: Email ID fehlt
Phone: Phone:
NotFound: Telefonnummer nicht gefunden NotFound: Telefonnummer nicht gefunden
Invalid: Telefonnummer ist ungültig Invalid: Telefonnummer ist ungültig
AlreadyVerified: Telefonnummer bereits verifiziert AlreadyVerified: Telefonnummer bereits verifiziert
Empty: Telefonnummer ist leer
NotChanged: Telefonnummer wurde nicht geändert
Address: Address:
NotFound: Adresse nicht gefunden NotFound: Adresse nicht gefunden
NotChanged: Adresse wurde nicht geändert NotChanged: Adresse wurde nicht geändert
@ -100,6 +107,7 @@ Errors:
Username: Username:
AlreadyExists: Benutzername ist bereits vergeben AlreadyExists: Benutzername ist bereits vergeben
Reserved: Benutzername ist bereits vergeben Reserved: Benutzername ist bereits vergeben
Empty: Benutzername ist leer
Code: Code:
Empty: Code ist leer Empty: Code ist leer
NotFound: Code konnte nicht gefunden werden NotFound: Code konnte nicht gefunden werden

View File

@ -70,16 +70,23 @@ Errors:
Profile: Profile:
NotFound: Profile not found NotFound: Profile not found
NotChanged: Profile not changed NotChanged: Profile not changed
Invalid: Profile data invalid Empty: Profile is empty
FirstNameEmpty: First name in profile is empty
LastNameEmpty: Last name in profile is empty
IDMissing: Profile ID is missing
Email: Email:
NotFound: Email not found NotFound: Email not found
Invalid: Email is invalid Invalid: Email is invalid
AlreadyVerified: Email is already verified AlreadyVerified: Email is already verified
NotChanged: Email not changed NotChanged: Email not changed
Empty: Email is empty
IDMissing: Email ID is missing
Phone: Phone:
NotFound: Phone not found NotFound: Phone not found
Invalid: Phone is invalid Invalid: Phone is invalid
AlreadyVerified: Phone already verified AlreadyVerified: Phone already verified
Empty: Phone is empty
NotChanged: Phone not changed
Address: Address:
NotFound: Address not found NotFound: Address not found
NotChanged: Address not changed NotChanged: Address not changed
@ -100,6 +107,7 @@ Errors:
Username: Username:
AlreadyExists: Username already taken AlreadyExists: Username already taken
Reserved: Username is already taken Reserved: Username is already taken
Empty: Username is empty
Code: Code:
Empty: Code is empty Empty: Code is empty
NotFound: Code not found NotFound: Code not found

View File

@ -70,16 +70,23 @@ Errors:
Profile: Profile:
NotFound: Profil non trouvé NotFound: Profil non trouvé
NotChanged: Le profil n'a pas changé NotChanged: Le profil n'a pas changé
Invalid: Données de profil non valides Empty: Profil est vide
FirstNameEmpty: Le prénom dans le profil est vide
LastNameEmpty: Le nom de famille dans le profil est vide
IDMissing: Profil ID manquant
Email: Email:
NotFound: Email non trouvé NotFound: Email non trouvé
Invalid: L'email n'est pas valide Invalid: L'email n'est pas valide
AlreadyVerified: L'adresse électronique est déjà vérifiée AlreadyVerified: L'adresse électronique est déjà vérifiée
NotChanged: L'adresse électronique n'a pas changé NotChanged: L'adresse électronique n'a pas changé
Empty: Email est vide
IDMissing: Email ID manquant
Phone: Phone:
Notfound: Téléphone non trouvé Notfound: Téléphone non trouvé
Invalid: Le téléphone n'est pas valide Invalid: Le téléphone n'est pas valide
AlreadyVerified: Téléphone déjà vérifié AlreadyVerified: Téléphone déjà vérifié
Empty: Téléphone est vide
NotChanged: Téléphone n'a pas changé
Address: Address:
NotFound: Adresse non trouvée NotFound: Adresse non trouvée
NotChanged: L'adresse n'a pas changé NotChanged: L'adresse n'a pas changé
@ -100,6 +107,7 @@ Errors:
Username: Username:
AlreadyExists: Nom d'utilisateur déjà pris AlreadyExists: Nom d'utilisateur déjà pris
Reserved: Le nom d'utilisateur est déjà pris Reserved: Le nom d'utilisateur est déjà pris
Empty: Le nom d'utilisateur est vide
Code: Code:
Empty: Le code est vide Empty: Le code est vide
NotFound: Code non trouvé NotFound: Code non trouvé

View File

@ -70,16 +70,23 @@ Errors:
Profile: Profile:
NotFound: Profilo non trovato NotFound: Profilo non trovato
NotChanged: Profilo non cambiato NotChanged: Profilo non cambiato
Invalid: Dati non sono validi Empty: Profilo è vuoto
FirstNameEmpty: Il nome nel profilo è vuoto
LastNameEmpty: Il cognome nel profilo è vuoto
IDMissing: Profilo ID mancante
Email: Email:
NotFound: Email non trovata NotFound: Email non trovata
Invalid: L'e-mail non è valida Invalid: L'e-mail non è valida
AlreadyVerified: L'e-mail è già verificata AlreadyVerified: L'e-mail è già verificata
NotChanged: Email non cambiata NotChanged: Email non cambiata
Empty: Email è vuota
IDMissing: Email ID mancante
Phone: Phone:
NotFound: Telefono non trovato NotFound: Telefono non trovato
Invalid: Il telefono non è valido Invalid: Il telefono non è valido
AlreadyVerified: Telefono già verificato AlreadyVerified: Telefono già verificato
Empty: Il telefono è vuoto
NotChanged: Telefono non cambiato
Address: Address:
NotFound: Indirizzo non trovato NotFound: Indirizzo non trovato
NotChanged: Indirizzo non cambiato NotChanged: Indirizzo non cambiato
@ -100,6 +107,7 @@ Errors:
Username: Username:
AlreadyExists: Nome utente già preso AlreadyExists: Nome utente già preso
Reserved: Il nome utente è già preso Reserved: Il nome utente è già preso
Empty: Il nome utente è vuoto
Code: Code:
Empty: Il codice è vuoto Empty: Il codice è vuoto
NotFound: Codice non trovato NotFound: Codice non trovato

View File

@ -70,16 +70,23 @@ Errors:
Profile: Profile:
NotFound: Profil nie znaleziony NotFound: Profil nie znaleziony
NotChanged: Profil nie zmieniony NotChanged: Profil nie zmieniony
Invalid: Nieprawidłowe dane profilu Empty: Profil jest pusty
FirstNameEmpty: Imię w profilu jest puste
LastNameEmpty: Nazwisko w profilu jest puste
IDMissing: Profil ID brakuje
Email: Email:
NotFound: Adres e-mail nie znaleziony NotFound: Adres e-mail nie znaleziony
Invalid: Adres e-mail jest nieprawidłowy Invalid: Adres e-mail jest nieprawidłowy
AlreadyVerified: Adres e-mail jest już zweryfikowany AlreadyVerified: Adres e-mail jest już zweryfikowany
NotChanged: Adres e-mail nie zmieniony NotChanged: Adres e-mail nie zmieniony
Empty: Adres e-mail jest pusty
IDMissing: Adres e-mail ID brakuje
Phone: Phone:
NotFound: Numer telefonu nie znaleziony NotFound: Numer telefonu nie znaleziony
Invalid: Numer telefonu jest nieprawidłowy Invalid: Numer telefonu jest nieprawidłowy
AlreadyVerified: Numer telefonu już zweryfikowany AlreadyVerified: Numer telefonu już zweryfikowany
Empty: Numer telefonu jest pusty
NotChanged: Numer telefonu nie zmieniony
Address: Address:
NotFound: Adres nie znaleziony NotFound: Adres nie znaleziony
NotChanged: Adres nie zmieniony NotChanged: Adres nie zmieniony
@ -100,6 +107,7 @@ Errors:
Username: Username:
AlreadyExists: Nazwa użytkownika jest już zajęta AlreadyExists: Nazwa użytkownika jest już zajęta
Reserved: Nazwa użytkownika jest już zajęta Reserved: Nazwa użytkownika jest już zajęta
Empty: Nazwa użytkownika jest pusty
Code: Code:
Empty: Kod jest pusty Empty: Kod jest pusty
NotFound: Kod nie znaleziony NotFound: Kod nie znaleziony

View File

@ -70,16 +70,23 @@ Errors:
Profile: Profile:
NotFound: 未找到个人资料 NotFound: 未找到个人资料
NotChanged: 个人资料未更改 NotChanged: 个人资料未更改
Invalid: 个人资料数据无效 Empty: 简介是空的
FirstNameEmpty: 简介中的名字是空的
LastNameEmpty: 简介中的姓氏是空的
IDMissing: 简介ID丢失
Email: Email:
NotFound: 电子邮件没有找到 NotFound: 电子邮件没有找到
Invalid: 电子邮件无效 Invalid: 电子邮件无效
AlreadyVerified: 电子邮件已经过验证 AlreadyVerified: 电子邮件已经过验证
NotChanged: 电子邮件未更改 NotChanged: 电子邮件未更改
Empty: 电子邮件是空的
IDMissing: 电子邮件ID丢失
Phone: Phone:
NotFound: 手机号码未找到 NotFound: 手机号码未找到
Invalid: 手机号码无效 Invalid: 手机号码无效
AlreadyVerified: 手机号码已经验证 AlreadyVerified: 手机号码已经验证
Empty: 电话号码是空的
NotChanged: 电话号码没有改变
Address: Address:
NotFound: 找不到地址 NotFound: 找不到地址
NotChanged: 地址没有改变 NotChanged: 地址没有改变
@ -100,6 +107,7 @@ Errors:
Username: Username:
AlreadyExists: 用户名已被使用 AlreadyExists: 用户名已被使用
Reserved: 用户名已被使用 Reserved: 用户名已被使用
Empty: 用户名是空的
Code: Code:
Empty: 验证码为空 Empty: 验证码为空
NotFound: 验证码不存在 NotFound: 验证码不存在

View File

@ -21,10 +21,6 @@ type EmailCode struct {
Expiry time.Duration Expiry time.Duration
} }
func (e *Email) IsValid() bool {
return e.EmailAddress != ""
}
func (e *Email) GenerateEmailCodeIfNeeded(emailGenerator crypto.Generator) (*EmailCode, error) { func (e *Email) GenerateEmailCodeIfNeeded(emailGenerator crypto.Generator) (*EmailCode, error) {
var emailCode *EmailCode var emailCode *EmailCode
if e.IsEmailVerified { if e.IsEmailVerified {

View File

@ -3,9 +3,7 @@ package model
import ( import (
"time" "time"
"github.com/ttacon/libphonenumber"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
caos_errs "github.com/zitadel/zitadel/internal/errors"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
) )
@ -27,20 +25,6 @@ type PhoneCode struct {
Expiry time.Duration 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.ThrowPreconditionFailed(nil, "EVENT-so0wa", "Phonenumber is invalid")
}
p.PhoneNumber = libphonenumber.Format(phoneNr, libphonenumber.E164)
return nil
}
func (p *Phone) GeneratePhoneCodeIfNeeded(phoneGenerator crypto.Generator) (*PhoneCode, error) { func (p *Phone) GeneratePhoneCodeIfNeeded(phoneGenerator crypto.Generator) (*PhoneCode, error) {
var phoneCode *PhoneCode var phoneCode *PhoneCode
if p.IsPhoneVerified { if p.IsPhoneVerified {

View File

@ -1,107 +0,0 @@
package model
import (
"testing"
caos_errs "github.com/zitadel/zitadel/internal/errors"
)
func TestFormatPhoneNumber(t *testing.T) {
type args struct {
phone *Phone
}
tests := []struct {
name string
args args
result *Phone
errFunc func(err error) bool
}{
{
name: "invalid phone number",
args: args{
phone: &Phone{
PhoneNumber: "PhoneNumber",
},
},
errFunc: caos_errs.IsPreconditionFailed,
},
{
name: "format phone 071...",
args: args{
phone: &Phone{
PhoneNumber: "0711234567",
},
},
result: &Phone{
PhoneNumber: "+41711234567",
},
},
{
name: "format phone 0041...",
args: args{
phone: &Phone{
PhoneNumber: "0041711234567",
},
},
result: &Phone{
PhoneNumber: "+41711234567",
},
},
{
name: "format phone 071 xxx xx xx",
args: args{
phone: &Phone{
PhoneNumber: "071 123 45 67",
},
},
result: &Phone{
PhoneNumber: "+41711234567",
},
},
{
name: "format phone +4171 xxx xx xx",
args: args{
phone: &Phone{
PhoneNumber: "+4171 123 45 67",
},
},
result: &Phone{
PhoneNumber: "+41711234567",
},
},
{
name: "format phone 004171 xxx xx xx",
args: args{
phone: &Phone{
PhoneNumber: "004171 123 45 67",
},
},
result: &Phone{
PhoneNumber: "+41711234567",
},
},
{
name: "format non swiss phone 004371 xxx xx xx",
args: args{
phone: &Phone{
PhoneNumber: "004371 123 45 67",
},
},
result: &Phone{
PhoneNumber: "+43711234567",
},
},
}
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)
}
if tt.errFunc != nil && !tt.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}