diff --git a/cmd/setup/03.go b/cmd/setup/03.go index 1fba41da23..3630ac85f6 100644 --- a/cmd/setup/03.go +++ b/cmd/setup/03.go @@ -91,11 +91,11 @@ func (mig *FirstInstance) Execute(ctx context.Context) error { 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.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 == "" { - mig.instanceSetup.Org.Human.Email.Address = mig.instanceSetup.Org.Human.Username - if !strings.Contains(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) + if !strings.Contains(string(mig.instanceSetup.Org.Human.Email.Address), "@") { + mig.instanceSetup.Org.Human.Email.Address = domain.EmailAddress(mig.instanceSetup.Org.Human.Username + "@" + domain.NewIAMDomainName(mig.instanceSetup.Org.Name, mig.instanceSetup.CustomDomain)) } } diff --git a/e2e/config/host.docker.internal/zitadel.yaml b/e2e/config/host.docker.internal/zitadel.yaml index 43fcc361e7..70f98b7797 100644 --- a/e2e/config/host.docker.internal/zitadel.yaml +++ b/e2e/config/host.docker.internal/zitadel.yaml @@ -3,7 +3,8 @@ ExternalSecure: false Database: 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: Enabled: false diff --git a/e2e/config/localhost/zitadel.yaml b/e2e/config/localhost/zitadel.yaml index 7fa4fca1b1..fc1c062460 100644 --- a/e2e/config/localhost/zitadel.yaml +++ b/e2e/config/localhost/zitadel.yaml @@ -3,7 +3,8 @@ ExternalSecure: false Database: 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: Enabled: false diff --git a/internal/actions/object/user.go b/internal/actions/object/user.go index 0c8ed07e91..860185828d 100644 --- a/internal/actions/object/user.go +++ b/internal/actions/object/user.go @@ -156,9 +156,9 @@ type human struct { AvatarKey string PreferredLanguage string Gender domain.Gender - Email string + Email domain.EmailAddress IsEmailVerified bool - Phone string + Phone domain.PhoneNumber IsPhoneVerified bool } diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index d42df360af..81f897dd58 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -564,13 +564,13 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w } if user.Human.Email != "" { dataUser.User.Email = &management_pb.ImportHumanUserRequest_Email{ - Email: user.Human.Email, + Email: string(user.Human.Email), IsEmailVerified: user.Human.IsEmailVerified, } } if user.Human.Phone != "" { dataUser.User.Phone = &management_pb.ImportHumanUserRequest_Phone{ - Phone: user.Human.Phone, + Phone: string(user.Human.Phone), IsPhoneVerified: user.Human.IsPhoneVerified, } } diff --git a/internal/api/grpc/admin/user_converter.go b/internal/api/grpc/admin/user_converter.go index 3be84e2f23..06a9519c50 100644 --- a/internal/api/grpc/admin/user_converter.go +++ b/internal/api/grpc/admin/user_converter.go @@ -6,6 +6,7 @@ import ( user_grpc "github.com/zitadel/zitadel/internal/api/grpc/user" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" 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 { return command.Email{ - Address: email.Email, + Address: domain.EmailAddress(email.Email), Verified: email.IsEmailVerified, } } @@ -39,7 +40,7 @@ func setUpOrgHumanPhoneToDomain(phone *admin_grpc.SetUpOrgRequest_Human_Phone) c return command.Phone{} } return command.Phone{ - Number: phone.Phone, + Number: domain.PhoneNumber(phone.Phone), Verified: phone.IsPhoneVerified, } } diff --git a/internal/api/grpc/auth/email_converter.go b/internal/api/grpc/auth/email_converter.go index 03b2b81369..e4ddbb61c0 100644 --- a/internal/api/grpc/auth/email_converter.go +++ b/internal/api/grpc/auth/email_converter.go @@ -10,6 +10,6 @@ import ( func UpdateMyEmailToDomain(ctx context.Context, email *auth.SetMyEmailRequest) *domain.Email { return &domain.Email{ ObjectRoot: ctxToObjectRoot(ctx), - EmailAddress: email.Email, + EmailAddress: domain.EmailAddress(email.Email), } } diff --git a/internal/api/grpc/auth/phone_converter.go b/internal/api/grpc/auth/phone_converter.go index 8cf514eada..23346446ae 100644 --- a/internal/api/grpc/auth/phone_converter.go +++ b/internal/api/grpc/auth/phone_converter.go @@ -10,6 +10,6 @@ import ( func UpdateMyPhoneToDomain(ctx context.Context, phone *auth.SetMyPhoneRequest) *domain.Phone { return &domain.Phone{ ObjectRoot: ctxToObjectRoot(ctx), - PhoneNumber: phone.Phone, + PhoneNumber: domain.PhoneNumber(phone.Phone), } } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 7fb6db5db6..035e9e833d 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -208,7 +208,7 @@ func AddHumanUserRequestToAddHuman(req *mgmt_pb.AddHumanUserRequest) *command.Ad NickName: req.Profile.NickName, DisplayName: req.Profile.DisplayName, Email: command.Email{ - Address: req.Email.Email, + Address: domain.EmailAddress(req.Email.Email), Verified: req.Email.IsEmailVerified, }, PreferredLanguage: lang, @@ -221,7 +221,7 @@ func AddHumanUserRequestToAddHuman(req *mgmt_pb.AddHumanUserRequest) *command.Ad } if req.Phone != nil { human.Phone = command.Phone{ - Number: req.Phone.Phone, + Number: domain.PhoneNumber(req.Phone.Phone), Verified: req.Phone.IsPhoneVerified, } } @@ -446,7 +446,7 @@ func (s *Server) ResendHumanInitialization(ctx context.Context, req *mgmt_pb.Res if err != nil { 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 { return nil, err } diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 43ef713514..3c969c2a1c 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -91,37 +91,6 @@ func ListUserMetadataToDomain(req *mgmt_pb.ListUserMetadataRequest) (*query.User }, 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) { human = &domain.Human{ Username: req.UserName, @@ -137,12 +106,12 @@ func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human Gender: user_grpc.GenderToDomain(req.Profile.Gender), } human.Email = &domain.Email{ - EmailAddress: req.Email.Email, + EmailAddress: domain.EmailAddress(req.Email.Email), IsEmailVerified: req.Email.IsEmailVerified, } if req.Phone != nil { human.Phone = &domain.Phone{ - PhoneNumber: req.Phone.Phone, + PhoneNumber: domain.PhoneNumber(req.Phone.Phone), IsPhoneVerified: req.Phone.IsPhoneVerified, } } @@ -199,7 +168,7 @@ func UpdateHumanEmailRequestToDomain(ctx context.Context, req *mgmt_pb.UpdateHum AggregateID: req.UserId, ResourceOwner: authz.GetCtxData(ctx).OrgID, }, - EmailAddress: req.Email, + EmailAddress: domain.EmailAddress(req.Email), IsEmailVerified: req.IsEmailVerified, } } @@ -207,7 +176,7 @@ func UpdateHumanEmailRequestToDomain(ctx context.Context, req *mgmt_pb.UpdateHum func UpdateHumanPhoneRequestToDomain(req *mgmt_pb.UpdateHumanPhoneRequest) *domain.Phone { return &domain.Phone{ ObjectRoot: models.ObjectRoot{AggregateID: req.UserId}, - PhoneNumber: req.Phone, + PhoneNumber: domain.PhoneNumber(req.Phone), IsPhoneVerified: req.IsPhoneVerified, } } diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go index 2e1a4933bd..304ab19c24 100644 --- a/internal/api/grpc/system/instance_converter.go +++ b/internal/api/grpc/system/instance_converter.go @@ -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 { user := defaultHuman if req.Email != nil { - user.Email.Address = req.Email.Email + user.Email.Address = domain.EmailAddress(req.Email.Email) user.Email.Verified = req.Email.IsEmailVerified } if req.Profile != nil { @@ -164,7 +164,7 @@ func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInst instance.Org.Human = new(command.AddHuman) } 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 } if req.OwnerProfile != nil { diff --git a/internal/api/grpc/user/converter.go b/internal/api/grpc/user/converter.go index 28cd367fb1..9d0cadd993 100644 --- a/internal/api/grpc/user/converter.go +++ b/internal/api/grpc/user/converter.go @@ -58,11 +58,11 @@ func HumanToPb(view *query.Human, assetPrefix, owner string) *user_pb.Human { AvatarUrl: domain.AvatarURL(assetPrefix, owner, view.AvatarKey), }, Email: &user_pb.Email{ - Email: view.Email, + Email: string(view.Email), IsEmailVerified: view.IsEmailVerified, }, Phone: &user_pb.Phone{ - Phone: view.Phone, + Phone: string(view.Phone), 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 { return &user_pb.Email{ - Email: email.Email, + Email: string(email.Email), IsEmailVerified: email.IsVerified, } } @@ -105,7 +105,7 @@ func PhoneToPb(phone *query.Phone) *user_pb.Phone { func ModelEmailToPb(email *query.Email) *user_pb.Email { return &user_pb.Email{ - Email: email.Email, + Email: string(email.Email), IsEmailVerified: email.IsVerified, } } diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 08d9b4f819..80dad0aec4 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -268,7 +268,7 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSette if user.Human == nil { continue } - userInfo.SetEmail(user.Human.Email, user.Human.IsEmailVerified) + userInfo.SetEmail(string(user.Human.Email), user.Human.IsEmailVerified) case oidc.ScopeProfile: userInfo.SetPreferredUsername(user.PreferredLoginName) userInfo.SetUpdatedAt(user.ChangeDate) @@ -287,7 +287,7 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSette if user.Human == nil { continue } - userInfo.SetPhone(user.Human.Phone, user.Human.IsPhoneVerified) + userInfo.SetPhone(string(user.Human.Phone), user.Human.IsPhoneVerified) case oidc.ScopeAddress: //TODO: handle address for human users as soon as implemented case ScopeUserMetaData: diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index 1c855a6671..b47b321ad4 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -154,7 +154,7 @@ func setUserinfo(user *query.User, userinfo models.AttributeSetter, attributes [ if user.Human == nil { return } - userinfo.SetEmail(user.Human.Email) + userinfo.SetEmail(string(user.Human.Email)) userinfo.SetSurname(user.Human.LastName) userinfo.SetGivenName(user.Human.FirstName) userinfo.SetFullName(user.Human.DisplayName) @@ -164,7 +164,7 @@ func setUserinfo(user *query.User, userinfo models.AttributeSetter, attributes [ switch attribute { case provider.AttributeEmail: if user.Human != nil { - userinfo.SetEmail(user.Human.Email) + userinfo.SetEmail(string(user.Human.Email)) } case provider.AttributeSurname: if user.Human != nil { diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go index 3a1dc6d890..0cbe59f118 100644 --- a/internal/api/ui/login/custom_action.go +++ b/internal/api/ui/login/custom_action.go @@ -55,13 +55,13 @@ func (l *Login) runPostExternalAuthenticationActions( actions.SetFields("setPreferredUsername", func(username string) { user.PreferredUsername = username }), - actions.SetFields("setEmail", func(email string) { + actions.SetFields("setEmail", func(email domain.EmailAddress) { user.Email = email }), actions.SetFields("setEmailVerified", func(verified bool) { user.IsEmailVerified = verified }), - actions.SetFields("setPhone", func(phone string) { + actions.SetFields("setPhone", func(phone domain.PhoneNumber) { user.Phone = phone }), actions.SetFields("setPhoneVerified", func(verified bool) { @@ -222,7 +222,7 @@ func (l *Login) runPreCreationActions( actions.SetFields("setUsername", func(username string) { user.Username = username }), - actions.SetFields("setEmail", func(email string) { + actions.SetFields("setEmail", func(email domain.EmailAddress) { if user.Email == nil { user.Email = &domain.Email{} } @@ -234,11 +234,11 @@ func (l *Login) runPreCreationActions( } user.Email.IsEmailVerified = verified }), - actions.SetFields("setPhone", func(email string) { + actions.SetFields("setPhone", func(phone domain.PhoneNumber) { if user.Phone == nil { user.Phone = &domain.Phone{} } - user.Phone.PhoneNumber = email + user.Phone.PhoneNumber = phone }), actions.SetFields("setPhoneVerified", func(verified bool) { if user.Phone == nil { diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index f887b57ee6..711c6572e8 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -60,28 +60,28 @@ type externalNotFoundOptionData struct { ShowUsername bool ShowUsernameSuffix bool OrgRegister bool - ExternalEmail string + ExternalEmail domain.EmailAddress ExternalEmailVerified bool - ExternalPhone string + ExternalPhone domain.PhoneNumber ExternalPhoneVerified bool } type externalRegisterFormData struct { - ExternalIDPConfigID string `schema:"external-idp-config-id"` - ExternalIDPExtUserID string `schema:"external-idp-ext-user-id"` - ExternalIDPDisplayName string `schema:"external-idp-display-name"` - ExternalEmail string `schema:"external-email"` - ExternalEmailVerified bool `schema:"external-email-verified"` - Email string `schema:"email"` - Username string `schema:"username"` - Firstname string `schema:"firstname"` - Lastname string `schema:"lastname"` - Nickname string `schema:"nickname"` - ExternalPhone string `schema:"external-phone"` - ExternalPhoneVerified bool `schema:"external-phone-verified"` - Phone string `schema:"phone"` - Language string `schema:"language"` - TermsConfirm bool `schema:"terms-confirm"` + ExternalIDPConfigID string `schema:"external-idp-config-id"` + ExternalIDPExtUserID string `schema:"external-idp-ext-user-id"` + ExternalIDPDisplayName string `schema:"external-idp-display-name"` + ExternalEmail domain.EmailAddress `schema:"external-email"` + ExternalEmailVerified bool `schema:"external-email-verified"` + Email domain.EmailAddress `schema:"email"` + Username string `schema:"username"` + Firstname string `schema:"firstname"` + Lastname string `schema:"lastname"` + Nickname string `schema:"nickname"` + ExternalPhone domain.PhoneNumber `schema:"external-phone"` + ExternalPhoneVerified bool `schema:"external-phone-verified"` + Phone domain.PhoneNumber `schema:"phone"` + Language string `schema:"language"` + TermsConfirm bool `schema:"terms-confirm"` } // handleExternalLoginStep is called as nextStep @@ -815,7 +815,7 @@ func mapExternalNotFoundOptionFormDataToLoginUser(formData *externalNotFoundOpti IDPConfigID: formData.ExternalIDPConfigID, ExternalUserID: formData.ExternalIDPExtUserID, PreferredUsername: formData.Username, - DisplayName: formData.Email, + DisplayName: string(formData.Email), FirstName: formData.Firstname, LastName: formData.Lastname, NickName: formData.Nickname, diff --git a/internal/api/ui/login/register_handler.go b/internal/api/ui/login/register_handler.go index bcc696be13..d2b4845db8 100644 --- a/internal/api/ui/login/register_handler.go +++ b/internal/api/ui/login/register_handler.go @@ -16,14 +16,14 @@ const ( ) type registerFormData struct { - Email string `schema:"email"` - Username string `schema:"username"` - Firstname string `schema:"firstname"` - Lastname string `schema:"lastname"` - Language string `schema:"language"` - Password string `schema:"register-password"` - Password2 string `schema:"register-password-confirmation"` - TermsConfirm bool `schema:"terms-confirm"` + Email domain.EmailAddress `schema:"email"` + Username string `schema:"username"` + Firstname string `schema:"firstname"` + Lastname string `schema:"lastname"` + Language string `schema:"language"` + Password string `schema:"register-password"` + Password2 string `schema:"register-password-confirmation"` + TermsConfirm bool `schema:"terms-confirm"` } type registerData struct { diff --git a/internal/api/ui/login/register_option_handler.go b/internal/api/ui/login/register_option_handler.go index 5488dc4918..b2d0c9e16f 100644 --- a/internal/api/ui/login/register_option_handler.go +++ b/internal/api/ui/login/register_option_handler.go @@ -94,7 +94,7 @@ func (l *Login) passLoginHintToRegistration(r *http.Request, authReq *domain.Aut if authReq == nil { return data } - data.Email = authReq.LoginHint + data.Email = domain.EmailAddress(authReq.LoginHint) domainPolicy, err := l.getOrgDomainPolicy(r, authReq.RequestedOrgID) if err != nil { logging.WithFields("authRequest", authReq.ID, "org", authReq.RequestedOrgID).Error("unable to load domain policy for registration loginHint") diff --git a/internal/api/ui/login/register_org_handler.go b/internal/api/ui/login/register_org_handler.go index 4dd332298a..065d9d0cd6 100644 --- a/internal/api/ui/login/register_org_handler.go +++ b/internal/api/ui/login/register_org_handler.go @@ -14,14 +14,14 @@ const ( ) type registerOrgFormData struct { - RegisterOrgName string `schema:"orgname"` - Email string `schema:"email"` - Username string `schema:"username"` - Firstname string `schema:"firstname"` - Lastname string `schema:"lastname"` - Password string `schema:"register-password"` - Password2 string `schema:"register-password-confirmation"` - TermsConfirm bool `schema:"terms-confirm"` + RegisterOrgName string `schema:"orgname"` + Email domain.EmailAddress `schema:"email"` + Username string `schema:"username"` + Firstname string `schema:"firstname"` + Lastname string `schema:"lastname"` + Password string `schema:"register-password"` + Password2 string `schema:"register-password-confirmation"` + TermsConfirm bool `schema:"terms-confirm"` } type registerOrgData struct { @@ -121,7 +121,7 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe func (d registerOrgFormData) toUserDomain() *domain.Human { if d.Username == "" { - d.Username = d.Email + d.Username = string(d.Email) } return &domain.Human{ Username: d.Username, @@ -140,7 +140,7 @@ func (d registerOrgFormData) toUserDomain() *domain.Human { func (d registerOrgFormData) toCommandOrg() *command.OrgSetup { if d.Username == "" { - d.Username = d.Email + d.Username = string(d.Email) } return &command.OrgSetup{ Name: d.RegisterOrgName, diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml index 301bf33b6c..29935a0fd6 100644 --- a/internal/api/ui/login/static/i18n/de.yaml +++ b/internal/api/ui/login/static/i18n/de.yaml @@ -328,6 +328,33 @@ Errors: Invalid: Userdaten sind ungültig DomainNotAllowedAsUsername: Domäne ist bereits reserviert und kann nicht verwendet 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: ConfirmationWrong: Passwort Bestätigung stimmt nicht überein Empty: Passwort ist leer diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml index d36b0bf5d2..bf98dc288a 100644 --- a/internal/api/ui/login/static/i18n/en.yaml +++ b/internal/api/ui/login/static/i18n/en.yaml @@ -328,6 +328,33 @@ Errors: Invalid: Invalid userdata DomainNotAllowedAsUsername: Domain is already reserved and cannot be used 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: ConfirmationWrong: Passwordconfirmation is wrong Empty: Password is empty diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml index afad746eb9..dfa35f983c 100644 --- a/internal/api/ui/login/static/i18n/fr.yaml +++ b/internal/api/ui/login/static/i18n/fr.yaml @@ -328,6 +328,33 @@ Errors: Invalid: Données utilisateur non valides 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 + 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: ConfirmationWrong: La confirmation du mot de passe est erronée Empty: Le mot de passe est vide diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml index 5eacf81b02..264b0a4af6 100644 --- a/internal/api/ui/login/static/i18n/it.yaml +++ b/internal/api/ui/login/static/i18n/it.yaml @@ -328,6 +328,33 @@ Errors: Invalid: I dati del utente non sono validi DomainNotAllowedAsUsername: Il dominio è già riservato e non può essere utilizzato 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: ConfirmationWrong: La conferma della password è sbagliata Empty: La password è vuota diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml index 2650463d6c..87376bd850 100644 --- a/internal/api/ui/login/static/i18n/pl.yaml +++ b/internal/api/ui/login/static/i18n/pl.yaml @@ -328,6 +328,33 @@ Errors: Invalid: Nieprawidłowe dane użytkownika 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 + 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: ConfirmationWrong: Potwierdzenie hasła jest niepoprawne Empty: Hasło jest puste diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml index da380c5ab4..5d82ea0e91 100644 --- a/internal/api/ui/login/static/i18n/zh.yaml +++ b/internal/api/ui/login/static/i18n/zh.yaml @@ -328,6 +328,33 @@ Errors: Invalid: 无效的用户数据 DomainNotAllowedAsUsername: 域名已存在,但无法使用 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: ConfirmationWrong: 密码不一致 Empty: 密码为空 diff --git a/internal/command/email.go b/internal/command/email.go index 89ca245692..4e2c3d973b 100644 --- a/internal/command/email.go +++ b/internal/command/email.go @@ -10,12 +10,12 @@ import ( ) type Email struct { - Address string + Address domain.EmailAddress Verified bool } -func (e *Email) Valid() bool { - return e.Address != "" && domain.EmailRegex.MatchString(e.Address) +func (e *Email) Validate() error { + return e.Address.Validate() } func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) { diff --git a/internal/command/phone.go b/internal/command/phone.go index ad28fa4371..109a60975e 100644 --- a/internal/command/phone.go +++ b/internal/command/phone.go @@ -4,30 +4,16 @@ import ( "context" "time" - "github.com/ttacon/libphonenumber" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/errors" ) type Phone struct { - Number string + Number domain.PhoneNumber 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) { return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg) } diff --git a/internal/command/phone_test.go b/internal/command/phone_test.go index 51e775d170..0dda258913 100644 --- a/internal/command/phone_test.go +++ b/internal/command/phone_test.go @@ -3,12 +3,13 @@ package command import ( "testing" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" ) func TestFormatPhoneNumber(t *testing.T) { type args struct { - number string + number domain.PhoneNumber } tests := []struct { name string @@ -44,10 +45,9 @@ func TestFormatPhoneNumber(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - formatted, err := FormatPhoneNumber(tt.args.number) - - if tt.errFunc == nil && tt.result.Number != formatted { - t.Errorf("got wrong result: expected: %v, actual: %v ", tt.args.number, formatted) + normalized, err := tt.args.number.Normalize() + if tt.errFunc == nil && tt.result.Number != normalized { + t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.Number, normalized) } if tt.errFunc != nil && !tt.errFunc(err) { t.Errorf("got wrong err: %v ", err) diff --git a/internal/command/user_human.go b/internal/command/user_human.go index f0b7fe8bf2..d11a793b8e 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -104,29 +104,31 @@ func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *Ad type humanCreationCommand interface { eventstore.Command - AddPhoneData(phoneNumber string) + AddPhoneData(phoneNumber domain.PhoneNumber) AddPasswordData(secret *crypto.CryptoValue, changeRequired bool) } func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if !human.Email.Valid() { - return nil, errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument") + if err := human.Email.Validate(); err != nil { + return nil, err } if human.Username = strings.TrimSpace(human.Username); human.Username == "" { return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument") } 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 == "" { - return nil, errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument") + return nil, errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty") } human.ensureDisplayName() - if human.Phone.Number, err = FormatPhoneNumber(human.Phone.Number); err != nil { - return nil, errors.ThrowInvalidArgument(nil, "USER-tD6ax", "Errors.Invalid.Argument") + if human.Phone.Number != "" { + 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) { @@ -387,19 +389,12 @@ func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domai 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) { - if orgID == "" || !human.IsValid() { - return nil, nil, nil, "", errors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid") + if orgID == "" { + 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) 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") } 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 == "") { - return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-9dk45", "Errors.User.Invalid") + if orgID == "" { + 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 != "" { human.Password.ChangeRequired = false @@ -441,7 +442,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. return nil, nil, err } human.Username = strings.TrimSpace(human.Username) - human.EmailAddress = strings.TrimSpace(human.EmailAddress) + human.EmailAddress = human.EmailAddress.Normalize() if !domainPolicy.UserLoginMustBeDomain { index := strings.LastIndex(human.Username, "@") if index > 1 { diff --git a/internal/command/user_human_email.go b/internal/command/user_human_email.go index 47d2fecbc7..22fbf7b464 100644 --- a/internal/command/user_human_email.go +++ b/internal/command/user_human_email.go @@ -13,8 +13,11 @@ import ( ) func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, emailCodeGenerator crypto.Generator) (*domain.Email, error) { - if !email.IsValid() || email.AggregateID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M9sf", "Errors.Email.Invalid") + if email.AggregateID == "" { + 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) diff --git a/internal/command/user_human_email_model.go b/internal/command/user_human_email_model.go index 4bba2127b8..f332477496 100644 --- a/internal/command/user_human_email_model.go +++ b/internal/command/user_human_email_model.go @@ -14,7 +14,7 @@ import ( type HumanEmailWriteModel struct { eventstore.WriteModel - Email string + Email domain.EmailAddress IsEmailVerified bool Code *crypto.CryptoValue @@ -95,7 +95,7 @@ func (wm *HumanEmailWriteModel) Query() *eventstore.SearchQueryBuilder { func (wm *HumanEmailWriteModel) NewChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - email string, + email domain.EmailAddress, ) (*user.HumanEmailChangedEvent, bool) { if wm.Email == email { return nil, false diff --git a/internal/command/user_human_init.go b/internal/command/user_human_init.go index 234b17249e..74deb4ceda 100644 --- a/internal/command/user_human_init.go +++ b/internal/command/user_human_init.go @@ -11,8 +11,8 @@ import ( "github.com/zitadel/zitadel/internal/repository/user" ) -//ResendInitialMail resend inital 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) { +// ResendInitialMail resend initial mail and changes email if provided +func (c *Commands) ResendInitialMail(ctx context.Context, userID string, email domain.EmailAddress, resourceOwner string, initCodeGenerator crypto.Generator) (objectDetails *domain.ObjectDetails, err error) { if userID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing") } diff --git a/internal/command/user_human_init_model.go b/internal/command/user_human_init_model.go index 9fd393cd4c..3ee4801bf4 100644 --- a/internal/command/user_human_init_model.go +++ b/internal/command/user_human_init_model.go @@ -14,7 +14,7 @@ import ( type HumanInitCodeWriteModel struct { eventstore.WriteModel - Email string + Email domain.EmailAddress IsEmailVerified bool Code *crypto.CryptoValue @@ -92,7 +92,7 @@ func (wm *HumanInitCodeWriteModel) Query() *eventstore.SearchQueryBuilder { func (wm *HumanInitCodeWriteModel) NewChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - email string, + email domain.EmailAddress, ) (*user.HumanEmailChangedEvent, bool) { changedEvent := user.NewHumanEmailChangedEvent(ctx, aggregate, email) return changedEvent, wm.Email != email diff --git a/internal/command/user_human_init_test.go b/internal/command/user_human_init_test.go index d63c1b59bb..838459bd2c 100644 --- a/internal/command/user_human_init_test.go +++ b/internal/command/user_human_init_test.go @@ -289,7 +289,7 @@ func TestCommandSide_ResendInitialMail(t *testing.T) { r := &Commands{ 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 { assert.NoError(t, err) } diff --git a/internal/command/user_human_model.go b/internal/command/user_human_model.go index 561f7d3d31..f2b4a4cf27 100644 --- a/internal/command/user_human_model.go +++ b/internal/command/user_human_model.go @@ -22,10 +22,10 @@ type HumanWriteModel struct { Gender domain.Gender Avatar string - Email string + Email domain.EmailAddress IsEmailVerified bool - Phone string + Phone domain.PhoneNumber IsPhoneVerified bool Country string diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go index 7970aa130e..3ee507f735 100644 --- a/internal/command/user_human_otp.go +++ b/internal/command/user_human_otp.go @@ -69,7 +69,7 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string accountName := domain.GenerateLoginName(human.GetUsername(), org.PrimaryDomain, orgPolicy.UserLoginMustBeDomain) if accountName == "" { - accountName = human.EmailAddress + accountName = string(human.EmailAddress) } key, secret, err := domain.NewOTPKey(c.multifactors.OTP.Issuer, accountName, c.multifactors.OTP.CryptoMFA) if err != nil { diff --git a/internal/command/user_human_phone.go b/internal/command/user_human_phone.go index 628bf01b6b..e548807693 100644 --- a/internal/command/user_human_phone.go +++ b/internal/command/user_human_phone.go @@ -14,10 +14,9 @@ import ( ) func (c *Commands) ChangeHumanPhone(ctx context.Context, phone *domain.Phone, resourceOwner string, phoneCodeGenerator crypto.Generator) (*domain.Phone, error) { - if !phone.IsValid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-6M0ds", "Errors.Phone.Invalid") + if err := phone.Normalize(); err != nil { + return nil, err } - existingPhone, err := c.phoneWriteModelByID(ctx, phone.AggregateID, resourceOwner) if err != nil { return nil, err diff --git a/internal/command/user_human_phone_model.go b/internal/command/user_human_phone_model.go index 17b0ae3757..0bbe16497c 100644 --- a/internal/command/user_human_phone_model.go +++ b/internal/command/user_human_phone_model.go @@ -14,7 +14,7 @@ import ( type HumanPhoneWriteModel struct { eventstore.WriteModel - Phone string + Phone domain.PhoneNumber IsPhoneVerified bool Code *crypto.CryptoValue @@ -107,7 +107,7 @@ func (wm *HumanPhoneWriteModel) Query() *eventstore.SearchQueryBuilder { func (wm *HumanPhoneWriteModel) NewChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - phone string, + phone domain.PhoneNumber, ) (*user.HumanPhoneChangedEvent, bool) { changedEvent := user.NewHumanPhoneChangedEvent(ctx, aggregate, phone) return changedEvent, phone != wm.Phone diff --git a/internal/command/user_human_profile.go b/internal/command/user_human_profile.go index a06d1a7289..e77f3288bc 100644 --- a/internal/command/user_human_profile.go +++ b/internal/command/user_human_profile.go @@ -9,10 +9,12 @@ import ( ) func (c *Commands) ChangeHumanProfile(ctx context.Context, profile *domain.Profile) (*domain.Profile, error) { - if !profile.IsValid() && profile.AggregateID != "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8io0d", "Errors.User.Profile.Invalid") + if profile.AggregateID == "" { + 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) if err != nil { return nil, err diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 5e9ed14c0c..b56ed01c1e 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -3445,7 +3445,7 @@ func newAddHumanEvent(password string, changeRequired bool, phone string) *user. changeRequired) } if phone != "" { - event.AddPhoneData(phone) + event.AddPhoneData(domain.PhoneNumber(phone)) } return event } @@ -3473,7 +3473,7 @@ func newRegisterHumanEvent(username, password string, changeRequired bool, phone changeRequired) } if phone != "" { - event.AddPhoneData(phone) + event.AddPhoneData(domain.PhoneNumber(phone)) } return event } @@ -3503,7 +3503,7 @@ func TestAddHumanCommand(t *testing.T) { }, }, 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{ - 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{ - ValidationErr: errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument"), + ValidationErr: errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"), }, }, { diff --git a/internal/command/user_human_webauthn.go b/internal/command/user_human_webauthn.go index e6d7b52eb3..2d9d8db7a8 100644 --- a/internal/command/user_human_webauthn.go +++ b/internal/command/user_human_webauthn.go @@ -155,7 +155,7 @@ func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner s } accountName := domain.GenerateLoginName(user.GetUsername(), org.PrimaryDomain, orgPolicy.UserLoginMustBeDomain) if accountName == "" { - accountName = user.EmailAddress + accountName = string(user.EmailAddress) } webAuthN, err := c.webauthnConfig.BeginRegistration(ctx, user, accountName, authenticatorPlatform, userVerification, isLoginUI, tokens...) if err != nil { diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index 38ff10fd6f..84ca2b9ab8 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -65,10 +65,10 @@ type ExternalUser struct { FirstName string LastName string NickName string - Email string + Email EmailAddress IsEmailVerified bool PreferredLanguage language.Tag - Phone string + Phone PhoneNumber IsPhoneVerified bool Metadatas []*Metadata } diff --git a/internal/domain/human.go b/internal/domain/human.go index 75a03bec32..677842d6aa 100644 --- a/internal/domain/human.go +++ b/internal/domain/human.go @@ -4,6 +4,7 @@ import ( "time" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/errors" caos_errors "github.com/zitadel/zitadel/internal/errors" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) @@ -60,8 +61,22 @@ func (f Gender) Specified() bool { return f > GenderUnspecified && f < genderCount } -func (u *Human) IsValid() bool { - return u.Username != "" && u.Profile != nil && u.Profile.IsValid() && u.Email != nil && u.Email.IsValid() && u.Phone == nil || (u.Phone != nil && u.Phone.PhoneNumber != "" && u.Phone.IsValid()) +func (u *Human) Normalize() error { + if u.Username == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Username.Empty") + } + if err := u.Profile.Validate(); err != nil { + return err + } + if err := u.Email.Validate(); err != nil { + return err + } + if u.Phone != nil && u.Phone.PhoneNumber != "" { + if err := u.Phone.Normalize(); err != nil { + return err + } + } + return nil } func (u *Human) CheckDomainPolicy(policy *DomainPolicy) error { @@ -69,7 +84,7 @@ func (u *Human) CheckDomainPolicy(policy *DomainPolicy) error { return caos_errors.ThrowPreconditionFailed(nil, "DOMAIN-zSH7j", "Errors.Users.DomainPolicyNil") } if !policy.UserLoginMustBeDomain && u.Profile != nil && u.Username == "" && u.Email != nil { - u.Username = u.EmailAddress + u.Username = string(u.EmailAddress) } return nil } diff --git a/internal/domain/human_email.go b/internal/domain/human_email.go index c0197bce2f..18bf0440d0 100644 --- a/internal/domain/human_email.go +++ b/internal/domain/human_email.go @@ -2,20 +2,38 @@ package domain import ( "regexp" + "strings" "time" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/errors" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) var ( - EmailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") ) +type EmailAddress string + +func (e EmailAddress) Validate() error { + if e == "" { + return errors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty") + } + if !emailRegex.MatchString(string(e)) { + return errors.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid") + } + return nil +} + +func (e EmailAddress) Normalize() EmailAddress { + return EmailAddress(strings.TrimSpace(string(e))) +} + type Email struct { es_models.ObjectRoot - EmailAddress string + EmailAddress EmailAddress IsEmailVerified bool } @@ -26,8 +44,11 @@ type EmailCode struct { Expiry time.Duration } -func (e *Email) IsValid() bool { - return e.EmailAddress != "" && EmailRegex.MatchString(e.EmailAddress) +func (e *Email) Validate() error { + if e == nil { + return errors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty") + } + return e.EmailAddress.Validate() } func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, error) { diff --git a/internal/domain/human_email_test.go b/internal/domain/human_email_test.go index 77474252c3..c516ade47d 100644 --- a/internal/domain/human_email_test.go +++ b/internal/domain/human_email_test.go @@ -65,7 +65,7 @@ func TestEmailValid(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := tt.args.email.IsValid() + result := tt.args.email.Validate() == nil if result != tt.result { t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, result) } diff --git a/internal/domain/human_phone.go b/internal/domain/human_phone.go index 81bcfd0d47..e6f8caa6d0 100644 --- a/internal/domain/human_phone.go +++ b/internal/domain/human_phone.go @@ -4,19 +4,31 @@ import ( "time" "github.com/ttacon/libphonenumber" + "github.com/zitadel/zitadel/internal/crypto" caos_errs "github.com/zitadel/zitadel/internal/errors" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) -const ( - defaultRegion = "CH" -) +const defaultRegion = "CH" + +type PhoneNumber string + +func (p PhoneNumber) Normalize() (PhoneNumber, error) { + if p == "" { + return p, caos_errs.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty") + } + phoneNr, err := libphonenumber.Parse(string(p), defaultRegion) + if err != nil { + return p, caos_errs.ThrowInvalidArgument(err, "PHONE-so0wa", "Errors.User.Phone.Invalid") + } + return PhoneNumber(libphonenumber.Format(phoneNr, libphonenumber.E164)), nil +} type Phone struct { es_models.ObjectRoot - PhoneNumber string + PhoneNumber PhoneNumber IsPhoneVerified bool } @@ -27,17 +39,16 @@ type PhoneCode struct { Expiry time.Duration } -func (p *Phone) IsValid() bool { - err := p.formatPhone() - return p.PhoneNumber != "" && err == nil -} - -func (p *Phone) formatPhone() error { - phoneNr, err := libphonenumber.Parse(p.PhoneNumber, defaultRegion) - if err != nil { - return caos_errs.ThrowInvalidArgument(nil, "EVENT-so0wa", "Errors.User.Phone.Invalid") +func (p *Phone) Normalize() error { + if p == nil { + return caos_errs.ThrowInvalidArgument(nil, "PHONE-YlbwO", "Errors.User.Phone.Empty") } - p.PhoneNumber = libphonenumber.Format(phoneNr, libphonenumber.E164) + normalizedNumber, err := p.PhoneNumber.Normalize() + if err != nil { + return err + } + // Issue for avoiding mutating state: https://github.com/zitadel/zitadel/issues/5412 + p.PhoneNumber = normalizedNumber return nil } diff --git a/internal/domain/human_phone_test.go b/internal/domain/human_phone_test.go index 3157dc3907..c16680de46 100644 --- a/internal/domain/human_phone_test.go +++ b/internal/domain/human_phone_test.go @@ -94,10 +94,9 @@ func TestFormatPhoneNumber(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.args.phone.formatPhone() - - if tt.errFunc == nil && tt.result.PhoneNumber != tt.args.phone.PhoneNumber { - t.Errorf("got wrong result: expected: %v, actual: %v ", tt.args.phone.PhoneNumber, tt.result.PhoneNumber) + normalized, err := tt.args.phone.PhoneNumber.Normalize() + if tt.errFunc == nil && tt.result.PhoneNumber != normalized { + t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.PhoneNumber, normalized) } if tt.errFunc != nil && !tt.errFunc(err) { t.Errorf("got wrong err: %v ", err) diff --git a/internal/domain/human_profile.go b/internal/domain/human_profile.go index c299e3d595..ac220c7758 100644 --- a/internal/domain/human_profile.go +++ b/internal/domain/human_profile.go @@ -3,6 +3,7 @@ package domain import ( "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/errors" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) @@ -19,8 +20,17 @@ type Profile struct { LoginNames []string } -func (p *Profile) IsValid() bool { - return p.FirstName != "" && p.LastName != "" +func (p *Profile) Validate() error { + if p == nil { + return errors.ThrowInvalidArgument(nil, "PROFILE-GPY3p", "Errors.User.Profile.Empty") + } + if p.FirstName == "" { + return errors.ThrowInvalidArgument(nil, "PROFILE-RF5z2", "Errors.User.Profile.FirstNameEmpty") + } + if p.LastName == "" { + return errors.ThrowInvalidArgument(nil, "PROFILE-DSUkN", "Errors.User.Profile.LastNameEmpty") + } + return nil } func AvatarURL(prefix, resourceOwner, key string) string { diff --git a/internal/idp/provider.go b/internal/idp/provider.go index db2808449e..8bce0e9a86 100644 --- a/internal/idp/provider.go +++ b/internal/idp/provider.go @@ -4,6 +4,8 @@ import ( "context" "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/domain" ) // Provider is the minimal implementation for a 3rd party authentication provider @@ -24,9 +26,9 @@ type User interface { GetDisplayName() string GetNickname() string GetPreferredUsername() string - GetEmail() string + GetEmail() domain.EmailAddress IsEmailVerified() bool - GetPhone() string + GetPhone() domain.PhoneNumber IsPhoneVerified() bool GetPreferredLanguage() language.Tag GetAvatarURL() string diff --git a/internal/idp/providers/azuread/azuread.go b/internal/idp/providers/azuread/azuread.go index b9cb50bf62..aede0ceff0 100644 --- a/internal/idp/providers/azuread/azuread.go +++ b/internal/idp/providers/azuread/azuread.go @@ -7,6 +7,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/idp" "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. // The verification can be automatically activated on the provider ([WithEmailVerified]) type User struct { - Sub string `json:"sub"` - FamilyName string `json:"family_name"` - GivenName string `json:"given_name"` - Name string `json:"name"` - PreferredUsername string `json:"preferred_username"` - Email string `json:"email"` - Picture string `json:"picture"` + Sub string `json:"sub"` + FamilyName string `json:"family_name"` + GivenName string `json:"given_name"` + Name string `json:"name"` + PreferredUsername string `json:"preferred_username"` + Email domain.EmailAddress `json:"email"` + Picture string `json:"picture"` isEmailVerified bool } @@ -162,7 +163,7 @@ func (u *User) GetPreferredUsername() string { } // GetEmail is an implementation of the [idp.User] interface. -func (u *User) GetEmail() string { +func (u *User) GetEmail() domain.EmailAddress { return u.Email } @@ -176,7 +177,7 @@ func (u *User) IsEmailVerified() bool { // GetPhone is an implementation of the [idp.User] interface. // 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 "" } diff --git a/internal/idp/providers/azuread/session_test.go b/internal/idp/providers/azuread/session_test.go index d86b809290..d49a0b4f87 100644 --- a/internal/idp/providers/azuread/session_test.go +++ b/internal/idp/providers/azuread/session_test.go @@ -14,6 +14,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/idp" "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.nickName, user.GetNickname()) 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.phone, user.GetPhone()) + a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.avatarURL, user.GetAvatarURL()) diff --git a/internal/idp/providers/github/github.go b/internal/idp/providers/github/github.go index c3d08fa19c..bba9a6d9cc 100644 --- a/internal/idp/providers/github/github.go +++ b/internal/idp/providers/github/github.go @@ -4,6 +4,8 @@ import ( "strconv" "time" + "github.com/zitadel/zitadel/internal/domain" + "golang.org/x/oauth2" "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 // https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user type User struct { - Login string `json:"login"` - ID int `json:"id"` - NodeId string `json:"node_id"` - AvatarUrl string `json:"avatar_url"` - GravatarId string `json:"gravatar_id"` - Url string `json:"url"` - HtmlUrl string `json:"html_url"` - FollowersUrl string `json:"followers_url"` - FollowingUrl string `json:"following_url"` - GistsUrl string `json:"gists_url"` - StarredUrl string `json:"starred_url"` - SubscriptionsUrl string `json:"subscriptions_url"` - OrganizationsUrl string `json:"organizations_url"` - ReposUrl string `json:"repos_url"` - EventsUrl string `json:"events_url"` - ReceivedEventsUrl string `json:"received_events_url"` - Type string `json:"type"` - SiteAdmin bool `json:"site_admin"` - Name string `json:"name"` - Company string `json:"company"` - Blog string `json:"blog"` - Location string `json:"location"` - Email string `json:"email"` - Hireable bool `json:"hireable"` - Bio string `json:"bio"` - TwitterUsername string `json:"twitter_username"` - PublicRepos int `json:"public_repos"` - PublicGists int `json:"public_gists"` - Followers int `json:"followers"` - Following int `json:"following"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - PrivateGists int `json:"private_gists"` - TotalPrivateRepos int `json:"total_private_repos"` - OwnedPrivateRepos int `json:"owned_private_repos"` - DiskUsage int `json:"disk_usage"` - Collaborators int `json:"collaborators"` - TwoFactorAuthentication bool `json:"two_factor_authentication"` + Login string `json:"login"` + ID int `json:"id"` + NodeId string `json:"node_id"` + AvatarUrl string `json:"avatar_url"` + GravatarId string `json:"gravatar_id"` + Url string `json:"url"` + HtmlUrl string `json:"html_url"` + FollowersUrl string `json:"followers_url"` + FollowingUrl string `json:"following_url"` + GistsUrl string `json:"gists_url"` + StarredUrl string `json:"starred_url"` + SubscriptionsUrl string `json:"subscriptions_url"` + OrganizationsUrl string `json:"organizations_url"` + ReposUrl string `json:"repos_url"` + EventsUrl string `json:"events_url"` + ReceivedEventsUrl string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + Name string `json:"name"` + Company string `json:"company"` + Blog string `json:"blog"` + Location string `json:"location"` + Email domain.EmailAddress `json:"email"` + Hireable bool `json:"hireable"` + Bio string `json:"bio"` + TwitterUsername string `json:"twitter_username"` + PublicRepos int `json:"public_repos"` + PublicGists int `json:"public_gists"` + Followers int `json:"followers"` + Following int `json:"following"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PrivateGists int `json:"private_gists"` + TotalPrivateRepos int `json:"total_private_repos"` + OwnedPrivateRepos int `json:"owned_private_repos"` + DiskUsage int `json:"disk_usage"` + Collaborators int `json:"collaborators"` + TwoFactorAuthentication bool `json:"two_factor_authentication"` Plan struct { Name string `json:"name"` Space int `json:"space"` @@ -150,7 +152,7 @@ func (u *User) GetPreferredUsername() string { } // GetEmail is an implementation of the [idp.User] interface. -func (u *User) GetEmail() string { +func (u *User) GetEmail() domain.EmailAddress { return u.Email } @@ -162,7 +164,7 @@ func (u *User) IsEmailVerified() bool { // GetPhone is an implementation of the [idp.User] interface. // 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 "" } diff --git a/internal/idp/providers/github/session_test.go b/internal/idp/providers/github/session_test.go index 43a770a29f..9c40a0bb1d 100644 --- a/internal/idp/providers/github/session_test.go +++ b/internal/idp/providers/github/session_test.go @@ -14,6 +14,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/idp" "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.nickName, user.GetNickname()) 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.phone, user.GetPhone()) + a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.avatarURL, user.GetAvatarURL()) diff --git a/internal/idp/providers/gitlab/session_test.go b/internal/idp/providers/gitlab/session_test.go index 8f9337abc7..ff5c218132 100644 --- a/internal/idp/providers/gitlab/session_test.go +++ b/internal/idp/providers/gitlab/session_test.go @@ -14,6 +14,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/domain" "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.nickName, user.GetNickname()) 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.phone, user.GetPhone()) + a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.avatarURL, user.GetAvatarURL()) diff --git a/internal/idp/providers/google/google.go b/internal/idp/providers/google/google.go index d850a173d2..4c1506c6a6 100644 --- a/internal/idp/providers/google/google.go +++ b/internal/idp/providers/google/google.go @@ -43,5 +43,5 @@ type User struct { // GetPreferredUsername implements the [idp.User] interface. // It returns the email, because Google does not return a username. func (u *User) GetPreferredUsername() string { - return u.GetEmail() + return string(u.GetEmail()) } diff --git a/internal/idp/providers/google/session_test.go b/internal/idp/providers/google/session_test.go index f4696da715..6387318d9f 100644 --- a/internal/idp/providers/google/session_test.go +++ b/internal/idp/providers/google/session_test.go @@ -14,6 +14,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/domain" "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.nickName, user.GetNickname()) 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.phone, user.GetPhone()) + a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.avatarURL, user.GetAvatarURL()) diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index fb79302db1..3c47d436ac 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/oidc/v2/pkg/oidc" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/idp" ) @@ -106,8 +107,8 @@ func (u *User) GetNickname() string { return u.IDTokenClaims.GetNickname() } -func (u *User) GetPhone() string { - return u.IDTokenClaims.GetPhoneNumber() +func (u *User) GetPhone() domain.PhoneNumber { + return domain.PhoneNumber(u.IDTokenClaims.GetPhoneNumber()) } func (u *User) IsPhoneVerified() bool { @@ -121,3 +122,7 @@ func (u *User) GetPreferredLanguage() language.Tag { func (u *User) GetAvatarURL() string { return u.IDTokenClaims.GetPicture() } + +func (u *User) GetEmail() domain.EmailAddress { + return domain.EmailAddress(u.IDTokenClaims.GetEmail()) +} diff --git a/internal/idp/providers/jwt/session_test.go b/internal/idp/providers/jwt/session_test.go index 61f5526543..4aca6692dd 100644 --- a/internal/idp/providers/jwt/session_test.go +++ b/internal/idp/providers/jwt/session_test.go @@ -17,6 +17,7 @@ import ( "gopkg.in/square/go-jose.v2" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" ) 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.nickName, user.GetNickname()) 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.phone, user.GetPhone()) + a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.avatarURL, user.GetAvatarURL()) diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go index d1491205c7..638f4f46e4 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -10,6 +10,7 @@ import ( "github.com/go-ldap/ldap/v3" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/domain" "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.nickNameAttribute), user.GetAttributeValue(s.Provider.preferredUsernameAttribute), - user.GetAttributeValue(s.Provider.emailAttribute), + domain.EmailAddress(user.GetAttributeValue(s.Provider.emailAttribute)), emailVerified, - user.GetAttributeValue(s.Provider.phoneAttribute), + domain.PhoneNumber(user.GetAttributeValue(s.Provider.phoneAttribute)), phoneVerified, language.Make(user.GetAttributeValue(s.Provider.preferredLanguageAttribute)), user.GetAttributeValue(s.Provider.avatarURLAttribute), diff --git a/internal/idp/providers/ldap/user.go b/internal/idp/providers/ldap/user.go index 3577d88fa7..6bd208d1a0 100644 --- a/internal/idp/providers/ldap/user.go +++ b/internal/idp/providers/ldap/user.go @@ -1,6 +1,10 @@ package ldap -import "golang.org/x/text/language" +import ( + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/domain" +) type User struct { id string @@ -9,9 +13,9 @@ type User struct { displayName string nickName string preferredUsername string - email string + email domain.EmailAddress emailVerified bool - phone string + phone domain.PhoneNumber phoneVerified bool preferredLanguage language.Tag avatarURL string @@ -25,9 +29,9 @@ func NewUser( displayName string, nickName string, preferredUsername string, - email string, + email domain.EmailAddress, emailVerified bool, - phone string, + phone domain.PhoneNumber, phoneVerified bool, preferredLanguage language.Tag, avatarURL string, @@ -68,13 +72,13 @@ func (u *User) GetNickname() string { func (u *User) GetPreferredUsername() string { return u.preferredUsername } -func (u *User) GetEmail() string { +func (u *User) GetEmail() domain.EmailAddress { return u.email } func (u *User) IsEmailVerified() bool { return u.emailVerified } -func (u *User) GetPhone() string { +func (u *User) GetPhone() domain.PhoneNumber { return u.phone } func (u *User) IsPhoneVerified() bool { diff --git a/internal/idp/providers/oauth/mapper.go b/internal/idp/providers/oauth/mapper.go index 3493ea4309..eea40fc5e5 100644 --- a/internal/idp/providers/oauth/mapper.go +++ b/internal/idp/providers/oauth/mapper.go @@ -7,6 +7,7 @@ import ( "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/idp" ) @@ -74,7 +75,7 @@ func (u *UserMapper) GetPreferredUsername() string { } // GetEmail is an implementation of the [idp.User] interface. -func (u *UserMapper) GetEmail() string { +func (u *UserMapper) GetEmail() domain.EmailAddress { return "" } @@ -84,7 +85,7 @@ func (u *UserMapper) IsEmailVerified() bool { } // GetPhone is an implementation of the [idp.User] interface. -func (u *UserMapper) GetPhone() string { +func (u *UserMapper) GetPhone() domain.PhoneNumber { return "" } diff --git a/internal/idp/providers/oauth/session_test.go b/internal/idp/providers/oauth/session_test.go index ed9c10defb..f5cfc36e1e 100644 --- a/internal/idp/providers/oauth/session_test.go +++ b/internal/idp/providers/oauth/session_test.go @@ -13,6 +13,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/domain" "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.nickName, user.GetNickname()) 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.phone, user.GetPhone()) + a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.avatarURL, user.GetAvatarURL()) diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index a70a189cd8..a2a668c0c8 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/oidc/v2/pkg/oidc" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/idp" ) @@ -82,8 +83,8 @@ func (u *User) GetDisplayName() string { return u.GetName() } -func (u *User) GetPhone() string { - return u.GetPhoneNumber() +func (u *User) GetPhone() domain.PhoneNumber { + return domain.PhoneNumber(u.GetPhoneNumber()) } func (u *User) IsPhoneVerified() bool { @@ -97,3 +98,7 @@ func (u *User) GetPreferredLanguage() language.Tag { func (u *User) GetAvatarURL() string { return u.GetPicture() } + +func (u *User) GetEmail() domain.EmailAddress { + return domain.EmailAddress(u.UserInfo.GetEmail()) +} diff --git a/internal/idp/providers/oidc/session_test.go b/internal/idp/providers/oidc/session_test.go index fe18a71b74..c84aaef33e 100644 --- a/internal/idp/providers/oidc/session_test.go +++ b/internal/idp/providers/oidc/session_test.go @@ -17,6 +17,7 @@ import ( "gopkg.in/square/go-jose.v2" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" "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.nickName, user.GetNickname()) 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.phone, user.GetPhone()) + a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone()) a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified()) a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage()) a.Equal(tt.want.avatarURL, user.GetAvatarURL()) diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index 4ef4014bff..64500fa297 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -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(HumanGenderCol, &sql.NullInt16{Int16: int16(e.Gender), Valid: e.Gender.Specified()}), 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), ), @@ -351,7 +351,7 @@ func (p *userProjection) reduceHumanAdded(event eventstore.Event) (*handler.Stat handler.NewCol(NotifyUserIDCol, e.Aggregate().ID), handler.NewCol(NotifyInstanceIDCol, e.Aggregate().InstanceID), 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), }, 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(HumanGenderCol, &sql.NullInt16{Int16: int16(e.Gender), Valid: e.Gender.Specified()}), 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), ), @@ -399,7 +399,7 @@ func (p *userProjection) reduceHumanRegistered(event eventstore.Event) (*handler handler.NewCol(NotifyUserIDCol, e.Aggregate().ID), handler.NewCol(NotifyInstanceIDCol, e.Aggregate().InstanceID), 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), }, crdb.WithTableSuffix(UserNotifySuffix), @@ -660,7 +660,7 @@ func (p *userProjection) reduceHumanPhoneChanged(event eventstore.Event) (*handl ), crdb.AddUpdateStatement( []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.NewCond(NotifyUserIDCol, e.Aggregate().ID), @@ -786,7 +786,7 @@ func (p *userProjection) reduceHumanEmailChanged(event eventstore.Event) (*handl ), crdb.AddUpdateStatement( []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.NewCond(NotifyUserIDCol, e.Aggregate().ID), diff --git a/internal/query/projection/user_test.go b/internal/query/projection/user_test.go index cddeca5734..53d1e81863 100644 --- a/internal/query/projection/user_test.go +++ b/internal/query/projection/user_test.go @@ -75,7 +75,7 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{String: "display-name", Valid: true}, &sql.NullString{String: "ch-DE", 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}, }, }, @@ -84,7 +84,7 @@ func TestUserProjection_reduces(t *testing.T) { expectedArgs: []interface{}{ "agg-id", "instance-id", - "email@zitadel.com", + domain.EmailAddress("email@zitadel.com"), &sql.NullString{String: "+41 00 000 00 00", Valid: true}, false, }, @@ -144,7 +144,7 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{String: "display-name", Valid: true}, &sql.NullString{String: "ch-DE", 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}, }, }, @@ -153,7 +153,7 @@ func TestUserProjection_reduces(t *testing.T) { expectedArgs: []interface{}{ "agg-id", "instance-id", - "email@zitadel.com", + domain.EmailAddress("email@zitadel.com"), &sql.NullString{String: "+41 00 000 00 00", Valid: true}, false, }, @@ -208,7 +208,7 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{}, &sql.NullString{String: "und", Valid: false}, &sql.NullInt16{}, - "email@zitadel.com", + domain.EmailAddress("email@zitadel.com"), &sql.NullString{}, }, }, @@ -217,7 +217,7 @@ func TestUserProjection_reduces(t *testing.T) { expectedArgs: []interface{}{ "agg-id", "instance-id", - "email@zitadel.com", + domain.EmailAddress("email@zitadel.com"), &sql.NullString{String: "", Valid: false}, false, }, @@ -277,7 +277,7 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{String: "display-name", Valid: true}, &sql.NullString{String: "ch-DE", 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}, }, }, @@ -286,7 +286,7 @@ func TestUserProjection_reduces(t *testing.T) { expectedArgs: []interface{}{ "agg-id", "instance-id", - "email@zitadel.com", + domain.EmailAddress("email@zitadel.com"), &sql.NullString{String: "+41 00 000 00 00", Valid: true}, false, }, @@ -346,7 +346,7 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{String: "display-name", Valid: true}, &sql.NullString{String: "ch-DE", 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}, }, }, @@ -355,7 +355,7 @@ func TestUserProjection_reduces(t *testing.T) { expectedArgs: []interface{}{ "agg-id", "instance-id", - "email@zitadel.com", + domain.EmailAddress("email@zitadel.com"), &sql.NullString{String: "+41 00 000 00 00", Valid: true}, false, }, @@ -410,7 +410,7 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{}, &sql.NullString{String: "und", Valid: false}, &sql.NullInt16{}, - "email@zitadel.com", + domain.EmailAddress("email@zitadel.com"), &sql.NullString{}, }, }, @@ -419,7 +419,7 @@ func TestUserProjection_reduces(t *testing.T) { expectedArgs: []interface{}{ "agg-id", "instance-id", - "email@zitadel.com", + domain.EmailAddress("email@zitadel.com"), &sql.NullString{String: "", Valid: 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)", expectedArgs: []interface{}{ - "+41 00 000 00 00", + domain.PhoneNumber("+41 00 000 00 00"), false, "agg-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)", expectedArgs: []interface{}{ - "+41 00 000 00 00", + domain.PhoneNumber("+41 00 000 00 00"), false, "agg-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)", expectedArgs: []interface{}{ - "email@zitadel.com", + domain.EmailAddress("email@zitadel.com"), false, "agg-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)", expectedArgs: []interface{}{ - "email@zitadel.com", + domain.EmailAddress("email@zitadel.com"), false, "agg-id", "instance-id", diff --git a/internal/query/user.go b/internal/query/user.go index 7790ce9b8d..1550a4678b 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -47,9 +47,9 @@ type Human struct { AvatarKey string PreferredLanguage language.Tag Gender domain.Gender - Email string + Email domain.EmailAddress IsEmailVerified bool - Phone string + Phone domain.PhoneNumber IsPhoneVerified bool } @@ -74,7 +74,7 @@ type Email struct { ChangeDate time.Time ResourceOwner string Sequence uint64 - Email string + Email domain.EmailAddress IsVerified bool } @@ -847,9 +847,9 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder AvatarKey: avatarKey.String, PreferredLanguage: language.Make(preferredLanguage.String), Gender: domain.Gender(gender.Int32), - Email: email.String, + Email: domain.EmailAddress(email.String), IsEmailVerified: isEmailVerified.Bool, - Phone: phone.String, + Phone: domain.PhoneNumber(phone.String), IsPhoneVerified: isPhoneVerified.Bool, } } 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") } - e.Email = email.String + e.Email = domain.EmailAddress(email.String) e.IsVerified = isEmailVerified.Bool return e, nil @@ -1318,9 +1318,9 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde AvatarKey: avatarKey.String, PreferredLanguage: language.Make(preferredLanguage.String), Gender: domain.Gender(gender.Int32), - Email: email.String, + Email: domain.EmailAddress(email.String), IsEmailVerified: isEmailVerified.Bool, - Phone: phone.String, + Phone: domain.PhoneNumber(phone.String), IsPhoneVerified: isPhoneVerified.Bool, } } else if machineID.Valid { diff --git a/internal/repository/user/human.go b/internal/repository/user/human.go index 5d3b598a98..6184b7d071 100644 --- a/internal/repository/user/human.go +++ b/internal/repository/user/human.go @@ -38,9 +38,9 @@ type HumanAddedEvent struct { PreferredLanguage language.Tag `json:"preferredLanguage,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"` Locality string `json:"locality,omitempty"` @@ -75,7 +75,7 @@ func (e *HumanAddedEvent) AddAddressData( } func (e *HumanAddedEvent) AddPhoneData( - phoneNumber string, + phoneNumber domain.PhoneNumber, ) { e.PhoneNumber = phoneNumber } @@ -99,7 +99,7 @@ func NewHumanAddedEvent( displayName string, preferredLanguage language.Tag, gender domain.Gender, - emailAddress string, + emailAddress domain.EmailAddress, userLoginMustBeDomain bool, ) *HumanAddedEvent { return &HumanAddedEvent{ @@ -133,30 +133,24 @@ func HumanAddedEventMapper(event *repository.Event) (eventstore.Event, error) { } type HumanRegisteredEvent struct { - eventstore.BaseEvent `json:"-"` - + eventstore.BaseEvent `json:"-"` UserName string `json:"userName"` userLoginMustBeDomain bool - - FirstName string `json:"firstName,omitempty"` - LastName string `json:"lastName,omitempty"` - NickName string `json:"nickName,omitempty"` - DisplayName string `json:"displayName,omitempty"` - PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"` - Gender domain.Gender `json:"gender,omitempty"` - - EmailAddress string `json:"email,omitempty"` - - PhoneNumber string `json:"phone,omitempty"` - - Country string `json:"country,omitempty"` - Locality string `json:"locality,omitempty"` - PostalCode string `json:"postalCode,omitempty"` - Region string `json:"region,omitempty"` - StreetAddress string `json:"streetAddress,omitempty"` - - Secret *crypto.CryptoValue `json:"secret,omitempty"` - ChangeRequired bool `json:"changeRequired,omitempty"` + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + NickName string `json:"nickName,omitempty"` + DisplayName string `json:"displayName,omitempty"` + PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"` + Gender domain.Gender `json:"gender,omitempty"` + EmailAddress domain.EmailAddress `json:"email,omitempty"` + PhoneNumber domain.PhoneNumber `json:"phone,omitempty"` + Country string `json:"country,omitempty"` + Locality string `json:"locality,omitempty"` + PostalCode string `json:"postalCode,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{} { @@ -182,7 +176,7 @@ func (e *HumanRegisteredEvent) AddAddressData( } func (e *HumanRegisteredEvent) AddPhoneData( - phoneNumber string, + phoneNumber domain.PhoneNumber, ) { e.PhoneNumber = phoneNumber } @@ -206,7 +200,7 @@ func NewHumanRegisteredEvent( displayName string, preferredLanguage language.Tag, gender domain.Gender, - emailAddress string, + emailAddress domain.EmailAddress, userLoginMustBeDomain bool, ) *HumanRegisteredEvent { return &HumanRegisteredEvent{ diff --git a/internal/repository/user/human_email.go b/internal/repository/user/human_email.go index bf42c21930..5732a7cac0 100644 --- a/internal/repository/user/human_email.go +++ b/internal/repository/user/human_email.go @@ -5,10 +5,10 @@ import ( "encoding/json" "time" - "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" ) @@ -24,7 +24,7 @@ const ( type HumanEmailChangedEvent struct { eventstore.BaseEvent `json:"-"` - EmailAddress string `json:"email,omitempty"` + EmailAddress domain.EmailAddress `json:"email,omitempty"` } func (e *HumanEmailChangedEvent) Data() interface{} { @@ -35,7 +35,7 @@ func (e *HumanEmailChangedEvent) UniqueConstraints() []*eventstore.EventUniqueCo 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{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, diff --git a/internal/repository/user/human_phone.go b/internal/repository/user/human_phone.go index b38342be89..ea5f4ac7c6 100644 --- a/internal/repository/user/human_phone.go +++ b/internal/repository/user/human_phone.go @@ -6,6 +6,7 @@ import ( "time" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" @@ -24,7 +25,7 @@ const ( type HumanPhoneChangedEvent struct { eventstore.BaseEvent `json:"-"` - PhoneNumber string `json:"phone,omitempty"` + PhoneNumber domain.PhoneNumber `json:"phone,omitempty"` } func (e *HumanPhoneChangedEvent) Data() interface{} { @@ -35,7 +36,7 @@ func (e *HumanPhoneChangedEvent) UniqueConstraints() []*eventstore.EventUniqueCo 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{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 29e00d82dd..d83b402eb2 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -69,17 +69,24 @@ Errors: UsernameNotChanged: Benutzername wurde nicht verändert Profile: NotFound: Profil nicht gefunden - NotChanged: Profile nicht verändert - Invalid: Profildaten sind ungültig + 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 @@ -100,6 +107,7 @@ Errors: Username: AlreadyExists: Benutzername ist bereits vergeben Reserved: Benutzername ist bereits vergeben + Empty: Benutzername ist leer Code: Empty: Code ist leer NotFound: Code konnte nicht gefunden werden diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index ab13bb4531..20a838be80 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -70,16 +70,23 @@ Errors: Profile: NotFound: Profile not found 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: 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 @@ -100,6 +107,7 @@ Errors: Username: AlreadyExists: Username already taken Reserved: Username is already taken + Empty: Username is empty Code: Empty: Code is empty NotFound: Code not found diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index e451dc36cd..36b8b0c328 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -70,16 +70,23 @@ Errors: Profile: NotFound: Profil non trouvé 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: 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é @@ -100,6 +107,7 @@ Errors: Username: AlreadyExists: Nom d'utilisateur déjà pris Reserved: Le nom d'utilisateur est déjà pris + Empty: Le nom d'utilisateur est vide Code: Empty: Le code est vide NotFound: Code non trouvé diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 91014813d8..f09487d59b 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -70,16 +70,23 @@ Errors: Profile: NotFound: Profilo non trovato 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: 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 @@ -100,6 +107,7 @@ Errors: Username: AlreadyExists: Nome utente già preso Reserved: Il nome utente è già preso + Empty: Il nome utente è vuoto Code: Empty: Il codice è vuoto NotFound: Codice non trovato diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index d5e3b56840..ee01942737 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -70,16 +70,23 @@ Errors: Profile: NotFound: Profil nie znaleziony 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: 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 @@ -100,6 +107,7 @@ Errors: Username: AlreadyExists: Nazwa użytkownika jest już zajęta Reserved: Nazwa użytkownika jest już zajęta + Empty: Nazwa użytkownika jest pusty Code: Empty: Kod jest pusty NotFound: Kod nie znaleziony diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index a2ef6092bd..fff1ad979a 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -70,16 +70,23 @@ Errors: Profile: NotFound: 未找到个人资料 NotChanged: 个人资料未更改 - Invalid: 个人资料数据无效 + Empty: 简介是空的 + FirstNameEmpty: 简介中的名字是空的 + LastNameEmpty: 简介中的姓氏是空的 + IDMissing: 简介ID丢失 Email: NotFound: 电子邮件没有找到 Invalid: 电子邮件无效 AlreadyVerified: 电子邮件已经过验证 NotChanged: 电子邮件未更改 + Empty: 电子邮件是空的 + IDMissing: 电子邮件ID丢失 Phone: NotFound: 手机号码未找到 Invalid: 手机号码无效 AlreadyVerified: 手机号码已经验证 + Empty: 电话号码是空的 + NotChanged: 电话号码没有改变 Address: NotFound: 找不到地址 NotChanged: 地址没有改变 @@ -100,6 +107,7 @@ Errors: Username: AlreadyExists: 用户名已被使用 Reserved: 用户名已被使用 + Empty: 用户名是空的 Code: Empty: 验证码为空 NotFound: 验证码不存在 diff --git a/internal/user/model/email.go b/internal/user/model/email.go index 1745f7ecc1..859b33e063 100644 --- a/internal/user/model/email.go +++ b/internal/user/model/email.go @@ -21,10 +21,6 @@ type EmailCode struct { Expiry time.Duration } -func (e *Email) IsValid() bool { - return e.EmailAddress != "" -} - func (e *Email) GenerateEmailCodeIfNeeded(emailGenerator crypto.Generator) (*EmailCode, error) { var emailCode *EmailCode if e.IsEmailVerified { diff --git a/internal/user/model/phone.go b/internal/user/model/phone.go index 61d3865983..f8f4ea1b4a 100644 --- a/internal/user/model/phone.go +++ b/internal/user/model/phone.go @@ -3,9 +3,7 @@ package model import ( "time" - "github.com/ttacon/libphonenumber" "github.com/zitadel/zitadel/internal/crypto" - caos_errs "github.com/zitadel/zitadel/internal/errors" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) @@ -27,20 +25,6 @@ type PhoneCode struct { Expiry time.Duration } -func (p *Phone) IsValid() bool { - err := p.formatPhone() - return p.PhoneNumber != "" && err == nil -} - -func (p *Phone) formatPhone() error { - phoneNr, err := libphonenumber.Parse(p.PhoneNumber, defaultRegion) - if err != nil { - return caos_errs.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) { var phoneCode *PhoneCode if p.IsPhoneVerified { diff --git a/internal/user/model/phone_test.go b/internal/user/model/phone_test.go deleted file mode 100644 index 0df607cd5d..0000000000 --- a/internal/user/model/phone_test.go +++ /dev/null @@ -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) - } - }) - } -}