diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 9814a40329..0798c7dd22 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -499,6 +499,7 @@ DefaultInstance: TOSLink: https://zitadel.com/docs/legal/terms-of-service PrivacyLink: https://zitadel.com/docs/legal/privacy-policy HelpLink: "" + SupportEmail: "" NotificationPolicy: PasswordChange: true LabelPolicy: diff --git a/console/src/app/modules/policies/login-texts/helper.ts b/console/src/app/modules/policies/login-texts/helper.ts index f71d75bfb9..d7fd2ed457 100644 --- a/console/src/app/modules/policies/login-texts/helper.ts +++ b/console/src/app/modules/policies/login-texts/helper.ts @@ -73,6 +73,7 @@ export function mapRequestValues(map: Partial, req: Req): Req { r3.setHelp(map.footerText?.help ?? ''); r3.setPrivacyPolicy(map.footerText?.privacyPolicy ?? ''); r3.setTos(map.footerText?.tos ?? ''); + r3.setSupportEmail(map.footerText?.supportEmail ?? ''); req.setFooterText(r3); const r4 = new InitMFADoneScreenText(); diff --git a/console/src/app/modules/policies/privacy-policy/privacy-policy.component.html b/console/src/app/modules/policies/privacy-policy/privacy-policy.component.html index 362b0f9544..70fd46927f 100644 --- a/console/src/app/modules/policies/privacy-policy/privacy-policy.component.html +++ b/console/src/app/modules/policies/privacy-policy/privacy-policy.component.html @@ -36,6 +36,12 @@ + + + {{ 'POLICY.PRIVACY_POLICY.SUPPORTEMAIL' | translate }} + + + diff --git a/console/src/app/modules/policies/privacy-policy/privacy-policy.component.scss b/console/src/app/modules/policies/privacy-policy/privacy-policy.component.scss index 507f364bcc..5f58269f24 100644 --- a/console/src/app/modules/policies/privacy-policy/privacy-policy.component.scss +++ b/console/src/app/modules/policies/privacy-policy/privacy-policy.component.scss @@ -7,9 +7,10 @@ display: grid; grid-template-columns: 1fr; column-gap: 1rem; + width: 75%; @media only screen and (min-width: 800px) { - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: 1fr 1fr; } } diff --git a/console/src/app/modules/policies/privacy-policy/privacy-policy.component.ts b/console/src/app/modules/policies/privacy-policy/privacy-policy.component.ts index 6444f3f802..a2b0f52b52 100644 --- a/console/src/app/modules/policies/privacy-policy/privacy-policy.component.ts +++ b/console/src/app/modules/policies/privacy-policy/privacy-policy.component.ts @@ -60,6 +60,7 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy { tosLink: ['', []], privacyLink: ['', []], helpLink: ['', []], + supportEmail: ['', []], }); this.canWrite$.pipe(take(1)).subscribe((canWrite) => { @@ -105,6 +106,7 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy { tosLink: '', privacyLink: '', helpLink: '', + supportEmail: '', }); } }) @@ -114,6 +116,7 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy { tosLink: '', privacyLink: '', helpLink: '', + supportEmail: '', }); }); } @@ -125,6 +128,7 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy { req.setPrivacyLink(this.form.get('privacyLink')?.value); req.setTosLink(this.form.get('tosLink')?.value); req.setHelpLink(this.form.get('helpLink')?.value); + req.setSupportEmail(this.form.get('supportEmail')?.value); (this.service as ManagementService) .addCustomPrivacyPolicy(req) .then(() => { @@ -137,6 +141,7 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy { req.setPrivacyLink(this.form.get('privacyLink')?.value); req.setTosLink(this.form.get('tosLink')?.value); req.setHelpLink(this.form.get('helpLink')?.value); + req.setSupportEmail(this.form.get('supportEmail')?.value); (this.service as ManagementService) .updateCustomPrivacyPolicy(req) @@ -151,6 +156,7 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy { req.setPrivacyLink(this.form.get('privacyLink')?.value); req.setTosLink(this.form.get('tosLink')?.value); req.setHelpLink(this.form.get('helpLink')?.value); + req.setSupportEmail(this.form.get('supportEmail')?.value); (this.service as AdminService) .updatePrivacyPolicy(req) diff --git a/console/src/app/modules/settings-list/settings-list.component.html b/console/src/app/modules/settings-list/settings-list.component.html index 74b535a463..5b24cac59e 100644 --- a/console/src/app/modules/settings-list/settings-list.component.html +++ b/console/src/app/modules/settings-list/settings-list.component.html @@ -53,6 +53,6 @@ - + diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 4b48c22db7..980e4f3123 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1185,6 +1185,7 @@ "TOSLINK": "Link zu den Allgemeinen Geschäftsbedingungen", "POLICYLINK": "Link zur den Datenschutzrichtlinien", "HELPLINK": "Link zur Hilfestellung", + "SUPPORTEMAIL": "Support E-Mail", "SAVED": "Saved successfully!", "RESET_TITLE": "Standardwerte wiederherstellen", "RESET_DESCRIPTION": "Sie sind im Begriff die Standardlinks für die AGBs und Datenschutzrichtlinie wiederherzustellen. Wollen Sie fortfahren?" diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 8c24142f44..41039461f0 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1186,6 +1186,7 @@ "TOSLINK": "Link to Terms of Service", "POLICYLINK": "Link to Privacy Policy", "HELPLINK": "Link to Help", + "SUPPORTEMAIL": "Support Email", "SAVED": "Saved successfully!", "RESET_TITLE": "Restore Default Values", "RESET_DESCRIPTION": "You are about to restore the default Links for TOS and Privacy Policy. Do you really want to continue?" diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 06f9a9b5e4..19aeaa8321 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1185,6 +1185,7 @@ "TOSLINK": "Lien vers les conditions d'utilisation", "POLICYLINK": "Lien vers la politique de confidentialité", "HELPLINK": "Lien vers l'aide", + "SUPPORTEMAIL": "E-mail d'assistance", "SAVED": "Enregistré avec succès !", "RESET_TITLE": "Restaurer les valeurs par défaut", "RESET_DESCRIPTION": "Vous êtes sur le point de restaurer les liens par défaut pour les CGS et la politique de confidentialité. Voulez-vous vraiment continuer ?" diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index ac7ed3323a..fdb623c5e3 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -8,7 +8,7 @@ }, "FOOTER": { "LINKS": { - "CONTACT": "Kontakt", + "CONTACT": "Contatto", "TOS": "Terms of Service", "PP": "Privacy Policy" }, @@ -1186,6 +1186,7 @@ "TOSLINK": "Link ai termini di servizio", "POLICYLINK": "Link all'informativa sulla privacy", "HELPLINK": "link per l'aiuto", + "SUPPORTEMAIL": "e-mail di supporto", "SAVED": "Salvato con successo!", "RESET_TITLE": "Ripristina i valori predefiniti", "RESET_DESCRIPTION": "Stai per ripristinare i link predefiniti per i TOS e l'informativa sulla privacy. Vuoi davvero continuare?" diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index e77e3cc92a..85d724c57a 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1185,6 +1185,7 @@ "TOSLINK": "Link do warunków korzystania", "POLICYLINK": "Link do polityki prywatności", "HELPLINK": "Link do pomocy", + "SUPPORTEMAIL": "E-mail wsparcia", "SAVED": "Pomyślnie zapisano!", "RESET_TITLE": "Przywróć wartości domyślne", "RESET_DESCRIPTION": "Masz zamiar przywrócić domyślne linki dla TOS i polityki prywatności. Czy na pewno chcesz kontynuować?" diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index bd989f7cca..f7d77cf61e 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1184,6 +1184,7 @@ "TOSLINK": "链接到服务条款", "POLICYLINK": "链接到隐私政策", "HELPLINK": "链接到帮助", + "SUPPORTEMAIL": "支持邮箱", "SAVED": "保存成功!", "RESET_TITLE": "恢复默认值", "RESET_DESCRIPTION": "您即将恢复 TOS 和隐私政策的默认链接。你真的要继续吗?" diff --git a/docs/docs/guides/manage/console/instance-settings.mdx b/docs/docs/guides/manage/console/instance-settings.mdx index d0c2e10fe8..1472365102 100644 --- a/docs/docs/guides/manage/console/instance-settings.mdx +++ b/docs/docs/guides/manage/console/instance-settings.mdx @@ -57,7 +57,11 @@ At the moment Twilio is available as SMS provider. You can configure on which changes the users will be notified. The text of the message can be changed in the [Message texts](#message-texts) -Notification +Notification ### SMTP @@ -132,7 +136,6 @@ Configure the different lifetimes checks for the login process: - **Second Factor Check Lifetime** specifies after which period a user has to revalidate the 2-Factor during the login process - **External Login Check Lifetime** specifies after which period a user has to revalidate the Multi Factor during the login process - ## Identity Providers You can configure all kinds of external identity providers for identity brokering, which support OIDC (OpenID Connect). @@ -190,7 +193,7 @@ You can either set this attribute on your whole ZITADEL instance or just on some ## Privacy Policy and TOS -With this setting you are able to configure your privacy policy, terms of service and help links. +With this setting you are able to configure your privacy policy, terms of service, help links and help/support email address. On register each user has to accept these policies. This policy can be also be overriden by your organizations. @@ -231,7 +234,11 @@ You can set the locale of the translations on the right. These are the texts for the login. Just like for message texts, you can select the locale on the right. -Login texts +Login texts ## OIDC token lifetimes and expiration diff --git a/docs/static/img/guides/console/privacypolicy.png b/docs/static/img/guides/console/privacypolicy.png index c957ebb854..4a5c9a96eb 100644 Binary files a/docs/static/img/guides/console/privacypolicy.png and b/docs/static/img/guides/console/privacypolicy.png differ diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 81f897dd58..800de828cb 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -521,9 +521,10 @@ func (s *Server) getPrivacyPolicy(ctx context.Context, orgID string) (_ *managem } if !queriedPrivacy.IsDefault { return &management_pb.AddCustomPrivacyPolicyRequest{ - TosLink: queriedPrivacy.TOSLink, - PrivacyLink: queriedPrivacy.PrivacyLink, - HelpLink: queriedPrivacy.HelpLink, + TosLink: queriedPrivacy.TOSLink, + PrivacyLink: queriedPrivacy.PrivacyLink, + HelpLink: queriedPrivacy.HelpLink, + SupportEmail: string(queriedPrivacy.SupportEmail), }, nil } return nil, nil diff --git a/internal/api/grpc/admin/privacy_policy_converter.go b/internal/api/grpc/admin/privacy_policy_converter.go index 829a3a0393..910267e14e 100644 --- a/internal/api/grpc/admin/privacy_policy_converter.go +++ b/internal/api/grpc/admin/privacy_policy_converter.go @@ -7,8 +7,9 @@ import ( func UpdatePrivacyPolicyToDomain(req *admin_pb.UpdatePrivacyPolicyRequest) *domain.PrivacyPolicy { return &domain.PrivacyPolicy{ - TOSLink: req.TosLink, - PrivacyLink: req.PrivacyLink, - HelpLink: req.HelpLink, + TOSLink: req.TosLink, + PrivacyLink: req.PrivacyLink, + HelpLink: req.HelpLink, + SupportEmail: domain.EmailAddress(req.SupportEmail), } } diff --git a/internal/api/grpc/management/policy_privacy_converter.go b/internal/api/grpc/management/policy_privacy_converter.go index 810ecda9c9..323c162142 100644 --- a/internal/api/grpc/management/policy_privacy_converter.go +++ b/internal/api/grpc/management/policy_privacy_converter.go @@ -7,16 +7,18 @@ import ( func AddPrivacyPolicyToDomain(req *mgmt_pb.AddCustomPrivacyPolicyRequest) *domain.PrivacyPolicy { return &domain.PrivacyPolicy{ - TOSLink: req.TosLink, - PrivacyLink: req.PrivacyLink, - HelpLink: req.HelpLink, + TOSLink: req.TosLink, + PrivacyLink: req.PrivacyLink, + HelpLink: req.HelpLink, + SupportEmail: domain.EmailAddress(req.SupportEmail), } } func UpdatePrivacyPolicyToDomain(req *mgmt_pb.UpdateCustomPrivacyPolicyRequest) *domain.PrivacyPolicy { return &domain.PrivacyPolicy{ - TOSLink: req.TosLink, - PrivacyLink: req.PrivacyLink, - HelpLink: req.HelpLink, + TOSLink: req.TosLink, + PrivacyLink: req.PrivacyLink, + HelpLink: req.HelpLink, + SupportEmail: domain.EmailAddress(req.SupportEmail), } } diff --git a/internal/api/grpc/policy/privacy_policy.go b/internal/api/grpc/policy/privacy_policy.go index 9f168199e8..f86bc48e3f 100644 --- a/internal/api/grpc/policy/privacy_policy.go +++ b/internal/api/grpc/policy/privacy_policy.go @@ -8,10 +8,11 @@ import ( func ModelPrivacyPolicyToPb(policy *query.PrivacyPolicy) *policy_pb.PrivacyPolicy { return &policy_pb.PrivacyPolicy{ - IsDefault: policy.IsDefault, - TosLink: policy.TOSLink, - PrivacyLink: policy.PrivacyLink, - HelpLink: policy.HelpLink, + IsDefault: policy.IsDefault, + TosLink: policy.TOSLink, + PrivacyLink: policy.PrivacyLink, + HelpLink: policy.HelpLink, + SupportEmail: string(policy.SupportEmail), Details: object.ToViewDetailsPb( policy.Sequence, policy.CreationDate, diff --git a/internal/api/grpc/text/custom_text.go b/internal/api/grpc/text/custom_text.go index 92b6f90097..0f3da00b53 100644 --- a/internal/api/grpc/text/custom_text.go +++ b/internal/api/grpc/text/custom_text.go @@ -458,6 +458,7 @@ func FooterTextToPb(text domain.FooterText) *text_pb.FooterText { Tos: text.TOS, PrivacyPolicy: text.PrivacyPolicy, Help: text.Help, + SupportEmail: text.SupportEmail, } } @@ -949,5 +950,6 @@ func FooterTextPbToDomain(text *text_pb.FooterText) domain.FooterText { TOS: text.Tos, PrivacyPolicy: text.PrivacyPolicy, Help: text.Help, + SupportEmail: text.SupportEmail, } } diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index b08d452ac2..7c9cf74c6c 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -442,6 +442,9 @@ func (l *Login) setLinksOnBaseData(baseData baseData, privacyPolicy *domain.Priv if link, err := templates.ParseTemplateText(privacyPolicy.HelpLink, lang); err == nil { baseData.HelpLink = link } + if link, err := templates.ParseTemplateText(string(privacyPolicy.SupportEmail), lang); err == nil { + baseData.SupportEmail = link + } return baseData } @@ -602,6 +605,7 @@ type baseData struct { TOSLink string PrivacyLink string HelpLink string + SupportEmail string AuthReqID string CSRF template.HTML Nonce string diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml index be2f1e4c95..b634fff2d5 100644 --- a/internal/api/ui/login/static/i18n/de.yaml +++ b/internal/api/ui/login/static/i18n/de.yaml @@ -172,6 +172,7 @@ PasswordChange: NewPasswordConfirmLabel: Passwort Bestätigung CancelButtonText: abbrechen NextButtonText: weiter + Footer: Fusszeile PasswordChangeDone: Title: Passwort ändern @@ -318,6 +319,7 @@ Footer: Tos: AGB PrivacyPolicy: Datenschutzerklärung Help: Hilfe + SupportEmail: Support E-Mail Errors: Internal: Es ist ein interner Fehler aufgetreten diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml index 2ca0b37d26..cf7de142fb 100644 --- a/internal/api/ui/login/static/i18n/en.yaml +++ b/internal/api/ui/login/static/i18n/en.yaml @@ -172,6 +172,7 @@ PasswordChange: NewPasswordConfirmLabel: Password confirmation CancelButtonText: cancel NextButtonText: next + Footer: Footer PasswordChangeDone: Title: Change Password @@ -318,6 +319,7 @@ Footer: Tos: TOS PrivacyPolicy: Privacy policy Help: Help + SupportEmail: Support E-mail Errors: Internal: An internal error occurred diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml index f19a3e0b79..f1eae1f15c 100644 --- a/internal/api/ui/login/static/i18n/fr.yaml +++ b/internal/api/ui/login/static/i18n/fr.yaml @@ -172,6 +172,7 @@ PasswordChange: NewPasswordConfirmLabel: Confirmation du mot de passe CancelButtonText: annuler NextButtonText: suivant + Footer: Bas de page PasswordChangeDone: Title: Changer le mot de passe @@ -318,6 +319,7 @@ Footer: Tos: TOS PrivacyPolicy: Politique de confidentialité Help: Aide + SupportEmail: E-mail d'assistance Errors: Internal: Une erreur interne s'est produite @@ -408,8 +410,8 @@ Errors: ExternalUserIDEmpty: L'ID de l'utilisateur externe est vide UserDisplayNameEmpty: Le nom d'affichage de l'utilisateur est vide NoExternalUserData: Aucune donnée d'utilisateur externe reçue - CreationNotAllowed : La création d'un nouvel utilisateur n'est pas autorisée sur ce fournisseur. - LinkingNotAllowed : La création d'un lien vers un utilisateur n'est pas autorisée pour ce fournisseur. + CreationNotAllowed: La création d'un nouvel utilisateur n'est pas autorisée sur ce fournisseur. + LinkingNotAllowed: La création d'un lien vers un utilisateur n'est pas autorisée pour ce fournisseur. GrantRequired: Connexion impossible. L'utilisateur doit avoir au moins une subvention sur l'application. Veuillez contacter votre administrateur. ProjectRequired: Connexion impossible. L'organisation de l'utilisateur doit être accordée au projet. Veuillez contacter votre administrateur. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml index 0e72249d9b..7231632c6e 100644 --- a/internal/api/ui/login/static/i18n/it.yaml +++ b/internal/api/ui/login/static/i18n/it.yaml @@ -172,6 +172,7 @@ PasswordChange: NewPasswordConfirmLabel: Conferma della password CancelButtonText: annulla NextButtonText: Avanti + Footer: Piè di pagina PasswordChangeDone: Title: Reimposta password @@ -318,6 +319,7 @@ Footer: Tos: Termini di servizio PrivacyPolicy: l'informativa sulla privacy Help: Aiuto + SupportEmail: E-mail di supporto Errors: Internal: Si è verificato un errore interno diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml index e8910efe5f..bdc4e33760 100644 --- a/internal/api/ui/login/static/i18n/pl.yaml +++ b/internal/api/ui/login/static/i18n/pl.yaml @@ -172,6 +172,7 @@ PasswordChange: NewPasswordConfirmLabel: Potwierdzenie hasła CancelButtonText: anuluj NextButtonText: dalej + Footer: Stopka PasswordChangeDone: Title: Zmiana hasła @@ -318,6 +319,7 @@ Footer: Tos: TOS PrivacyPolicy: Polityka prywatności Help: Pomoc + SupportEmail: E-mail wsparcia Errors: Internal: Wewnętrzny błąd diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml index 112c99c2e8..80f9e4f0a9 100644 --- a/internal/api/ui/login/static/i18n/zh.yaml +++ b/internal/api/ui/login/static/i18n/zh.yaml @@ -172,6 +172,7 @@ PasswordChange: NewPasswordConfirmLabel: 确认密码 CancelButtonText: 取消 NextButtonText: 继续 + Footer: 页脚 PasswordChangeDone: Title: 更改密码 @@ -318,6 +319,7 @@ Footer: Tos: 服务条款 PrivacyPolicy: 隐私政策 Help: 帮助 + SupportEmail: 支持邮箱 Errors: Internal: 发生了内部错误 diff --git a/internal/api/ui/login/static/templates/footer.html b/internal/api/ui/login/static/templates/footer.html index e4f7578b51..9df79949d3 100644 --- a/internal/api/ui/login/static/templates/footer.html +++ b/internal/api/ui/login/static/templates/footer.html @@ -1,20 +1,41 @@ {{define "footer"}} {{end}} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 7c77b61639..4b1ca88910 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -1158,6 +1158,7 @@ func privacyPolicyToDomain(p *query.PrivacyPolicy) *domain.PrivacyPolicy { TOSLink: p.TOSLink, PrivacyLink: p.PrivacyLink, HelpLink: p.HelpLink, + SupportEmail: p.SupportEmail, } } diff --git a/internal/command/custom_login_text.go b/internal/command/custom_login_text.go index 6802a8d666..21147c616d 100644 --- a/internal/command/custom_login_text.go +++ b/internal/command/custom_login_text.go @@ -1093,6 +1093,10 @@ func (c *Commands) createFooterTextEvents(ctx context.Context, agg *eventstore.A if event != nil { events = append(events, event) } + event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyFooterSupportEmail, existingText.FooterSupportEmail, text.Footer.SupportEmail, text.Language, defaultText) + if event != nil { + events = append(events, event) + } return events } diff --git a/internal/command/custom_login_text_model.go b/internal/command/custom_login_text_model.go index c38893342c..0672092260 100644 --- a/internal/command/custom_login_text_model.go +++ b/internal/command/custom_login_text_model.go @@ -289,7 +289,7 @@ type CustomLoginTextReadModel struct { FooterTOS string FooterPrivacyPolicy string FooterHelp string - FooterHelpLink string + FooterSupportEmail string } func (wm *CustomLoginTextReadModel) Reduce() error { @@ -2515,6 +2515,10 @@ func (wm *CustomLoginTextReadModel) handleFooterTextSetEvent(e *policy.CustomTex wm.FooterHelp = e.Text return } + if e.Key == domain.LoginKeyFooterSupportEmail { + wm.FooterSupportEmail = e.Text + return + } } func (wm *CustomLoginTextReadModel) handleFooterTextRemoveEvent(e *policy.CustomTextRemovedEvent) { @@ -2530,4 +2534,8 @@ func (wm *CustomLoginTextReadModel) handleFooterTextRemoveEvent(e *policy.Custom wm.FooterHelp = "" return } + if e.Key == domain.LoginKeyFooterSupportEmail { + wm.FooterSupportEmail = "" + return + } } diff --git a/internal/command/instance.go b/internal/command/instance.go index 230607dc93..436b61b2bf 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -87,9 +87,10 @@ type InstanceSetup struct { PasswordChange bool } PrivacyPolicy struct { - TOSLink string - PrivacyLink string - HelpLink string + TOSLink string + PrivacyLink string + HelpLink string + SupportEmail domain.EmailAddress } LabelPolicy struct { PrimaryColor string @@ -242,7 +243,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str prepareAddSecondFactorToDefaultLoginPolicy(instanceAgg, domain.SecondFactorTypeU2F), prepareAddMultiFactorToDefaultLoginPolicy(instanceAgg, domain.MultiFactorTypeU2FWithPIN), - prepareAddDefaultPrivacyPolicy(instanceAgg, setup.PrivacyPolicy.TOSLink, setup.PrivacyPolicy.PrivacyLink, setup.PrivacyPolicy.HelpLink), + prepareAddDefaultPrivacyPolicy(instanceAgg, setup.PrivacyPolicy.TOSLink, setup.PrivacyPolicy.PrivacyLink, setup.PrivacyPolicy.HelpLink, setup.PrivacyPolicy.SupportEmail), prepareAddDefaultNotificationPolicy(instanceAgg, setup.NotificationPolicy.PasswordChange), prepareAddDefaultLockoutPolicy(instanceAgg, setup.LockoutPolicy.MaxAttempts, setup.LockoutPolicy.ShouldShowLockoutFailure), diff --git a/internal/command/instance_converter.go b/internal/command/instance_converter.go index 2eda0acb4e..d4e334ef2f 100644 --- a/internal/command/instance_converter.go +++ b/internal/command/instance_converter.go @@ -127,6 +127,7 @@ func writeModelToPrivacyPolicy(wm *PrivacyPolicyWriteModel) *domain.PrivacyPolic TOSLink: wm.TOSLink, PrivacyLink: wm.PrivacyLink, HelpLink: wm.HelpLink, + SupportEmail: wm.SupportEmail, } } diff --git a/internal/command/instance_custom_login_text.go b/internal/command/instance_custom_login_text.go index 9c99963b4b..7b332b0a15 100644 --- a/internal/command/instance_custom_login_text.go +++ b/internal/command/instance_custom_login_text.go @@ -56,7 +56,6 @@ func (c *Commands) setCustomInstanceLoginText(ctx context.Context, instanceAgg * if !text.IsValid() { return nil, nil, caos_errs.ThrowInvalidArgument(nil, "Instance-kd9fs", "Errors.CustomText.Invalid") } - existingLoginText, err := c.defaultLoginTextWriteModelByID(ctx, text.Language) if err != nil { return nil, nil, err diff --git a/internal/command/instance_custom_login_text_test.go b/internal/command/instance_custom_login_text_test.go index e1a1c407b4..e3fbe1119b 100644 --- a/internal/command/instance_custom_login_text_test.go +++ b/internal/command/instance_custom_login_text_test.go @@ -1370,6 +1370,12 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, "Help", language.English, ), ), + eventFromEventPusherWithInstanceID( + "INSTANCE", + instance.NewCustomTextSetEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, "Support Email", language.English, + ), + ), }, ), ), @@ -1664,6 +1670,7 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { TOS: "TOS", PrivacyPolicy: "PrivacyPolicy", Help: "Help", + SupportEmail: "Support Email", }, }, }, @@ -2993,6 +3000,12 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, "Help", language.English, ), ), + eventFromEventPusherWithInstanceID( + "INSTANCE", + instance.NewCustomTextSetEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, "Support Email", language.English, + ), + ), ), expectPush( []*repository.Event{ @@ -4310,6 +4323,12 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, language.English, ), ), + eventFromEventPusherWithInstanceID( + "INSTANCE", + instance.NewCustomTextRemovedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, language.English, + ), + ), }, ), ), @@ -5681,6 +5700,12 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, "Help", language.English, ), ), + eventFromEventPusherWithInstanceID( + "INSTANCE", + instance.NewCustomTextSetEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, "Support Email", language.English, + ), + ), eventFromEventPusherWithInstanceID( "INSTANCE", instance.NewCustomTextRemovedEvent(context.Background(), @@ -6996,6 +7021,12 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, language.English, ), ), + eventFromEventPusherWithInstanceID( + "INSTANCE", + instance.NewCustomTextRemovedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, language.English, + ), + ), ), expectPush( []*repository.Event{ @@ -8313,6 +8344,12 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, "Help", language.English, ), ), + eventFromEventPusherWithInstanceID( + "INSTANCE", + instance.NewCustomTextSetEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, "Support Email", language.English, + ), + ), }, ), ), @@ -8607,6 +8644,7 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { TOS: "TOS", PrivacyPolicy: "PrivacyPolicy", Help: "Help", + SupportEmail: "Support Email", }, }, }, diff --git a/internal/command/instance_policy_privacy.go b/internal/command/instance_policy_privacy.go index 141b327658..5768108475 100644 --- a/internal/command/instance_policy_privacy.go +++ b/internal/command/instance_policy_privacy.go @@ -12,9 +12,9 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func (c *Commands) AddDefaultPrivacyPolicy(ctx context.Context, tosLink, privacyLink, helpLink string) (*domain.ObjectDetails, error) { +func (c *Commands) AddDefaultPrivacyPolicy(ctx context.Context, tosLink, privacyLink, helpLink string, supportEmail domain.EmailAddress) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddDefaultPrivacyPolicy(instanceAgg, tosLink, privacyLink, helpLink)) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddDefaultPrivacyPolicy(instanceAgg, tosLink, privacyLink, helpLink, supportEmail)) if err != nil { return nil, err } @@ -26,6 +26,13 @@ func (c *Commands) AddDefaultPrivacyPolicy(ctx context.Context, tosLink, privacy } func (c *Commands) ChangeDefaultPrivacyPolicy(ctx context.Context, policy *domain.PrivacyPolicy) (*domain.PrivacyPolicy, error) { + if policy.SupportEmail != "" { + if err := policy.SupportEmail.Validate(); err != nil { + return nil, err + } + policy.SupportEmail = policy.SupportEmail.Normalize() + } + existingPolicy, err := c.defaultPrivacyPolicyWriteModelByID(ctx) if err != nil { return nil, err @@ -35,7 +42,7 @@ func (c *Commands) ChangeDefaultPrivacyPolicy(ctx context.Context, policy *domai } instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.PrivacyPolicyWriteModel.WriteModel) - changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, instanceAgg, policy.TOSLink, policy.PrivacyLink, policy.HelpLink) + changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, instanceAgg, policy.TOSLink, policy.PrivacyLink, policy.HelpLink, policy.SupportEmail) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-9jJfs", "Errors.IAM.PrivacyPolicy.NotChanged") } @@ -81,8 +88,15 @@ func prepareAddDefaultPrivacyPolicy( tosLink, privacyLink, helpLink string, + supportEmail domain.EmailAddress, ) preparation.Validation { return func() (preparation.CreateCommands, error) { + if supportEmail != "" { + if err := supportEmail.Validate(); err != nil { + return nil, err + } + supportEmail = supportEmail.Normalize() + } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { writeModel := NewInstancePrivacyPolicyWriteModel(ctx) events, err := filter(ctx, writeModel.Query()) @@ -97,7 +111,7 @@ func prepareAddDefaultPrivacyPolicy( return nil, caos_errs.ThrowAlreadyExists(nil, "INSTANCE-M00rJ", "Errors.Instance.PrivacyPolicy.AlreadyExists") } return []eventstore.Command{ - instance.NewPrivacyPolicyAddedEvent(ctx, &a.Aggregate, tosLink, privacyLink, helpLink), + instance.NewPrivacyPolicyAddedEvent(ctx, &a.Aggregate, tosLink, privacyLink, helpLink, supportEmail), }, nil }, nil } diff --git a/internal/command/instance_policy_privacy_model.go b/internal/command/instance_policy_privacy_model.go index d83e61fd7a..24263a05de 100644 --- a/internal/command/instance_policy_privacy_model.go +++ b/internal/command/instance_policy_privacy_model.go @@ -4,6 +4,7 @@ import ( "context" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/policy" @@ -57,6 +58,7 @@ func (wm *InstancePrivacyPolicyWriteModel) NewChangedEvent( tosLink, privacyLink, helpLink string, + supportEmail domain.EmailAddress, ) (*instance.PrivacyPolicyChangedEvent, bool) { changes := make([]policy.PrivacyPolicyChanges, 0) @@ -69,6 +71,9 @@ func (wm *InstancePrivacyPolicyWriteModel) NewChangedEvent( if wm.HelpLink != helpLink { changes = append(changes, policy.ChangeHelpLink(helpLink)) } + if wm.SupportEmail != supportEmail { + changes = append(changes, policy.ChangeSupportEmail(supportEmail)) + } if len(changes) == 0 { return nil, false } diff --git a/internal/command/instance_policy_privacy_test.go b/internal/command/instance_policy_privacy_test.go index 707afa8300..eddf8a0c18 100644 --- a/internal/command/instance_policy_privacy_test.go +++ b/internal/command/instance_policy_privacy_test.go @@ -22,10 +22,11 @@ func TestCommandSide_AddDefaultPrivacyPolicy(t *testing.T) { eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - tosLink string - privacyLink string - helpLink string + ctx context.Context + tosLink string + privacyLink string + helpLink string + supportEmail domain.EmailAddress } type res struct { want *domain.ObjectDetails @@ -49,16 +50,18 @@ func TestCommandSide_AddDefaultPrivacyPolicy(t *testing.T) { "TOSLink", "PrivacyLink", "HelpLink", + "support@example.com", ), ), ), ), }, args: args{ - ctx: context.Background(), - tosLink: "TOSLink", - privacyLink: "PrivacyLink", - helpLink: "HelpLink", + ctx: context.Background(), + tosLink: "TOSLink", + privacyLink: "PrivacyLink", + helpLink: "HelpLink", + supportEmail: "support@example.com", }, res: res{ err: caos_errs.IsErrorAlreadyExists, @@ -79,6 +82,7 @@ func TestCommandSide_AddDefaultPrivacyPolicy(t *testing.T) { "TOSLink", "PrivacyLink", "HelpLink", + "support@example.com", ), ), }, @@ -86,10 +90,11 @@ func TestCommandSide_AddDefaultPrivacyPolicy(t *testing.T) { ), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - tosLink: "TOSLink", - privacyLink: "PrivacyLink", - helpLink: "HelpLink", + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + tosLink: "TOSLink", + privacyLink: "PrivacyLink", + helpLink: "HelpLink", + supportEmail: "support@example.com", }, res: res{ want: &domain.ObjectDetails{ @@ -97,6 +102,24 @@ func TestCommandSide_AddDefaultPrivacyPolicy(t *testing.T) { }, }, }, + { + name: "wrong email, can't add policy", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + tosLink: "TOSLink", + privacyLink: "PrivacyLink", + helpLink: "HelpLink", + supportEmail: "wrong email", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, { name: "add empty policy,ok", fields: fields{ @@ -112,6 +135,7 @@ func TestCommandSide_AddDefaultPrivacyPolicy(t *testing.T) { "", "", "", + "", ), ), }, @@ -119,10 +143,11 @@ func TestCommandSide_AddDefaultPrivacyPolicy(t *testing.T) { ), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - tosLink: "", - privacyLink: "", - helpLink: "", + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + tosLink: "", + privacyLink: "", + helpLink: "", + supportEmail: "", }, res: res{ want: &domain.ObjectDetails{ @@ -136,7 +161,7 @@ func TestCommandSide_AddDefaultPrivacyPolicy(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, } - got, err := r.AddDefaultPrivacyPolicy(tt.args.ctx, tt.args.tosLink, tt.args.privacyLink, tt.args.helpLink) + got, err := r.AddDefaultPrivacyPolicy(tt.args.ctx, tt.args.tosLink, tt.args.privacyLink, tt.args.helpLink, tt.args.supportEmail) if tt.res.err == nil { assert.NoError(t, err) } @@ -179,9 +204,10 @@ func TestCommandSide_ChangeDefaultPrivacyPolicy(t *testing.T) { args: args{ ctx: context.Background(), policy: &domain.PrivacyPolicy{ - TOSLink: "TOSLink", - PrivacyLink: "PrivacyLink", - HelpLink: "HelpLink", + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "support@example.com", }, }, res: res{ @@ -200,6 +226,7 @@ func TestCommandSide_ChangeDefaultPrivacyPolicy(t *testing.T) { "TOSLink", "PrivacyLink", "HelpLink", + "support@example.com", ), ), ), @@ -208,15 +235,36 @@ func TestCommandSide_ChangeDefaultPrivacyPolicy(t *testing.T) { args: args{ ctx: context.Background(), policy: &domain.PrivacyPolicy{ - TOSLink: "TOSLink", - PrivacyLink: "PrivacyLink", - HelpLink: "HelpLink", + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "support@example.com", }, }, res: res{ err: caos_errs.IsPreconditionFailed, }, }, + { + name: "wrong email, can't change policy", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "wrong email", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, { name: "change, ok", fields: fields{ @@ -229,6 +277,7 @@ func TestCommandSide_ChangeDefaultPrivacyPolicy(t *testing.T) { "TOSLink", "PrivacyLink", "HelpLink", + "support@example.com", ), ), ), @@ -239,6 +288,7 @@ func TestCommandSide_ChangeDefaultPrivacyPolicy(t *testing.T) { "TOSLinkChanged", "PrivacyLinkChanged", "HelpLinkChanged", + "support2@example.com", ), ), }, @@ -248,9 +298,10 @@ func TestCommandSide_ChangeDefaultPrivacyPolicy(t *testing.T) { args: args{ ctx: context.Background(), policy: &domain.PrivacyPolicy{ - TOSLink: "TOSLinkChanged", - PrivacyLink: "PrivacyLinkChanged", - HelpLink: "HelpLinkChanged", + TOSLink: "TOSLinkChanged", + PrivacyLink: "PrivacyLinkChanged", + HelpLink: "HelpLinkChanged", + SupportEmail: "support2@example.com", }, }, res: res{ @@ -259,9 +310,10 @@ func TestCommandSide_ChangeDefaultPrivacyPolicy(t *testing.T) { AggregateID: "INSTANCE", ResourceOwner: "INSTANCE", }, - TOSLink: "TOSLinkChanged", - PrivacyLink: "PrivacyLinkChanged", - HelpLink: "HelpLinkChanged", + TOSLink: "TOSLinkChanged", + PrivacyLink: "PrivacyLinkChanged", + HelpLink: "HelpLinkChanged", + SupportEmail: "support2@example.com", }, }, }, @@ -285,13 +337,14 @@ func TestCommandSide_ChangeDefaultPrivacyPolicy(t *testing.T) { } } -func newDefaultPrivacyPolicyChangedEvent(ctx context.Context, tosLink, privacyLink, helpLink string) *instance.PrivacyPolicyChangedEvent { +func newDefaultPrivacyPolicyChangedEvent(ctx context.Context, tosLink, privacyLink, helpLink, supportEmail string) *instance.PrivacyPolicyChangedEvent { event, _ := instance.NewPrivacyPolicyChangedEvent(ctx, &instance.NewAggregate("INSTANCE").Aggregate, []policy.PrivacyPolicyChanges{ policy.ChangeTOSLink(tosLink), policy.ChangePrivacyLink(privacyLink), policy.ChangeHelpLink(helpLink), + policy.ChangeSupportEmail(domain.EmailAddress(supportEmail)), }, ) return event diff --git a/internal/command/org_converter.go b/internal/command/org_converter.go index 7533f6963e..667dae1984 100644 --- a/internal/command/org_converter.go +++ b/internal/command/org_converter.go @@ -50,5 +50,6 @@ func orgWriteModelToPrivacyPolicy(wm *OrgPrivacyPolicyWriteModel) *domain.Privac TOSLink: wm.TOSLink, PrivacyLink: wm.PrivacyLink, HelpLink: wm.HelpLink, + SupportEmail: wm.SupportEmail, } } diff --git a/internal/command/org_custom_login_text_test.go b/internal/command/org_custom_login_text_test.go index 24e055738a..3c5cb207e2 100644 --- a/internal/command/org_custom_login_text_test.go +++ b/internal/command/org_custom_login_text_test.go @@ -1161,6 +1161,11 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, "Help", language.English, ), ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, "Support Email", language.English, + ), + ), }, ), ), @@ -1455,6 +1460,7 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { TOS: "TOS", PrivacyPolicy: "PrivacyPolicy", Help: "Help", + SupportEmail: "Support Email", }, }, }, @@ -2560,6 +2566,11 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, "Help", language.English, ), ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, "Support Email", language.English, + ), + ), ), expectPush( []*repository.Event{ @@ -3653,6 +3664,11 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, language.English, ), ), + eventFromEventPusher( + org.NewCustomTextRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, language.English, + ), + ), }, ), ), @@ -4800,6 +4816,11 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, "Help", language.English, ), ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, "Support Email", language.English, + ), + ), eventFromEventPusher( org.NewCustomTextRemovedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeySelectAccountTitle, language.English, @@ -5890,6 +5911,11 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, language.English, ), ), + eventFromEventPusher( + org.NewCustomTextRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, language.English, + ), + ), ), expectPush( []*repository.Event{ @@ -6983,6 +7009,11 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterHelp, "Help", language.English, ), ), + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyFooterSupportEmail, "Support Email", language.English, + ), + ), }, ), ), @@ -7277,6 +7308,7 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { TOS: "TOS", PrivacyPolicy: "PrivacyPolicy", Help: "Help", + SupportEmail: "Support Email", }, }, }, diff --git a/internal/command/org_policy_privacy.go b/internal/command/org_policy_privacy.go index 4385ee22ed..04f730f8f1 100644 --- a/internal/command/org_policy_privacy.go +++ b/internal/command/org_policy_privacy.go @@ -29,6 +29,14 @@ func (c *Commands) orgPrivacyPolicyWriteModelByID(ctx context.Context, orgID str } func (c *Commands) AddPrivacyPolicy(ctx context.Context, resourceOwner string, policy *domain.PrivacyPolicy) (*domain.PrivacyPolicy, error) { + + if policy.SupportEmail != "" { + if err := policy.SupportEmail.Validate(); err != nil { + return nil, err + } + policy.SupportEmail = policy.SupportEmail.Normalize() + } + if resourceOwner == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-MMk9fs", "Errors.ResourceOwnerMissing") } @@ -49,7 +57,8 @@ func (c *Commands) AddPrivacyPolicy(ctx context.Context, resourceOwner string, p orgAgg, policy.TOSLink, policy.PrivacyLink, - policy.HelpLink)) + policy.HelpLink, + policy.SupportEmail)) if err != nil { return nil, err } @@ -61,12 +70,19 @@ func (c *Commands) AddPrivacyPolicy(ctx context.Context, resourceOwner string, p } func (c *Commands) ChangePrivacyPolicy(ctx context.Context, resourceOwner string, policy *domain.PrivacyPolicy) (*domain.PrivacyPolicy, error) { + + if policy.SupportEmail != "" { + if err := policy.SupportEmail.Validate(); err != nil { + return nil, err + } + policy.SupportEmail = policy.SupportEmail.Normalize() + } + if resourceOwner == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-22N89f", "Errors.ResourceOwnerMissing") } - existingPolicy := NewOrgPrivacyPolicyWriteModel(resourceOwner) - err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) + existingPolicy, err := c.orgPrivacyPolicyWriteModelByID(ctx, resourceOwner) if err != nil { return nil, err } @@ -75,7 +91,7 @@ func (c *Commands) ChangePrivacyPolicy(ctx context.Context, resourceOwner string } orgAgg := OrgAggregateFromWriteModel(&existingPolicy.PrivacyPolicyWriteModel.WriteModel) - changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.TOSLink, policy.PrivacyLink, policy.HelpLink) + changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.TOSLink, policy.PrivacyLink, policy.HelpLink, policy.SupportEmail) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-4N9fs", "Errors.Org.PrivacyPolicy.NotChanged") } diff --git a/internal/command/org_policy_privacy_model.go b/internal/command/org_policy_privacy_model.go index acff496263..262935e0ff 100644 --- a/internal/command/org_policy_privacy_model.go +++ b/internal/command/org_policy_privacy_model.go @@ -3,8 +3,8 @@ package command import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/policy" ) @@ -59,6 +59,7 @@ func (wm *OrgPrivacyPolicyWriteModel) NewChangedEvent( tosLink, privacyLink, helpLink string, + supportEmail domain.EmailAddress, ) (*org.PrivacyPolicyChangedEvent, bool) { changes := make([]policy.PrivacyPolicyChanges, 0) @@ -71,6 +72,9 @@ func (wm *OrgPrivacyPolicyWriteModel) NewChangedEvent( if wm.HelpLink != helpLink { changes = append(changes, policy.ChangeHelpLink(helpLink)) } + if wm.SupportEmail != supportEmail { + changes = append(changes, policy.ChangeSupportEmail(supportEmail)) + } if len(changes) == 0 { return nil, false } diff --git a/internal/command/org_policy_privacy_test.go b/internal/command/org_policy_privacy_test.go index ac9ba384b2..986e7a0213 100644 --- a/internal/command/org_policy_privacy_test.go +++ b/internal/command/org_policy_privacy_test.go @@ -44,9 +44,10 @@ func TestCommandSide_AddPrivacyPolicy(t *testing.T) { args: args{ ctx: context.Background(), policy: &domain.PrivacyPolicy{ - TOSLink: "TOSLink", - PrivacyLink: "PrivacyLink", - HelpLink: "HelpLink", + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "support@example.com", }, }, res: res{ @@ -65,6 +66,7 @@ func TestCommandSide_AddPrivacyPolicy(t *testing.T) { "TOSLink", "PrivacyLink", "HelpLink", + "support@example.com", ), ), ), @@ -74,9 +76,10 @@ func TestCommandSide_AddPrivacyPolicy(t *testing.T) { ctx: context.Background(), orgID: "org1", policy: &domain.PrivacyPolicy{ - TOSLink: "TOSLink", - PrivacyLink: "PrivacyLink", - HelpLink: "HelpLink", + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "support@example.com", }, }, res: res{ @@ -97,6 +100,7 @@ func TestCommandSide_AddPrivacyPolicy(t *testing.T) { "TOSLink", "PrivacyLink", "HelpLink", + "support@example.com", ), ), }, @@ -107,9 +111,10 @@ func TestCommandSide_AddPrivacyPolicy(t *testing.T) { ctx: context.Background(), orgID: "org1", policy: &domain.PrivacyPolicy{ - TOSLink: "TOSLink", - PrivacyLink: "PrivacyLink", - HelpLink: "HelpLink", + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "support@example.com", }, }, res: res{ @@ -118,14 +123,36 @@ func TestCommandSide_AddPrivacyPolicy(t *testing.T) { AggregateID: "org1", ResourceOwner: "org1", }, - TOSLink: "TOSLink", - PrivacyLink: "PrivacyLink", - HelpLink: "HelpLink", + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "support@example.com", }, }, }, { - name: "add policy empty links, ok", + name: "wrong email, can't add policy", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "wrong email", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "add policy empty links and empty support email, ok", fields: fields{ eventstore: eventstoreExpect( t, @@ -138,6 +165,7 @@ func TestCommandSide_AddPrivacyPolicy(t *testing.T) { "", "", "", + "", ), ), }, @@ -148,9 +176,10 @@ func TestCommandSide_AddPrivacyPolicy(t *testing.T) { ctx: context.Background(), orgID: "org1", policy: &domain.PrivacyPolicy{ - TOSLink: "", - PrivacyLink: "", - HelpLink: "", + TOSLink: "", + PrivacyLink: "", + HelpLink: "", + SupportEmail: "", }, }, res: res{ @@ -159,9 +188,10 @@ func TestCommandSide_AddPrivacyPolicy(t *testing.T) { AggregateID: "org1", ResourceOwner: "org1", }, - TOSLink: "", - PrivacyLink: "", - HelpLink: "", + TOSLink: "", + PrivacyLink: "", + HelpLink: "", + SupportEmail: "", }, }, }, @@ -214,9 +244,10 @@ func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { args: args{ ctx: context.Background(), policy: &domain.PrivacyPolicy{ - TOSLink: "TOSLink", - PrivacyLink: "PrivacyLink", - HelpLink: "HelpLink", + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "support@example.com", }, }, res: res{ @@ -235,9 +266,10 @@ func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { ctx: context.Background(), orgID: "org1", policy: &domain.PrivacyPolicy{ - TOSLink: "TOSLink", - PrivacyLink: "PrivacyLink", - HelpLink: "HelpLink", + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "support@example.com", }, }, res: res{ @@ -256,6 +288,7 @@ func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { "TOSLink", "PrivacyLink", "HelpLink", + "support@example.com", ), ), ), @@ -265,15 +298,32 @@ func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { ctx: context.Background(), orgID: "org1", policy: &domain.PrivacyPolicy{ - TOSLink: "TOSLink", - PrivacyLink: "PrivacyLink", - HelpLink: "HelpLink", + TOSLink: "TOSLink", + PrivacyLink: "PrivacyLink", + HelpLink: "HelpLink", + SupportEmail: "support@example.com", }, }, res: res{ err: caos_errs.IsPreconditionFailed, }, }, + { + name: "wrong email, can't change policy", + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PrivacyPolicy{ + TOSLink: "TOSLinkChange", + PrivacyLink: "PrivacyLinkChange", + HelpLink: "HelpLinkChange", + SupportEmail: "wrong email", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, { name: "change, ok", fields: fields{ @@ -286,13 +336,14 @@ func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { "TOSLink", "PrivacyLink", "HelpLink", + "support@example.com", ), ), ), expectPush( []*repository.Event{ eventFromEventPusher( - newPrivacyPolicyChangedEvent(context.Background(), "org1", "TOSLinkChange", "PrivacyLinkChange", "HelpLinkChange"), + newPrivacyPolicyChangedEvent(context.Background(), "org1", "TOSLinkChange", "PrivacyLinkChange", "HelpLinkChange", "support2@example.com"), ), }, ), @@ -302,9 +353,10 @@ func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { ctx: context.Background(), orgID: "org1", policy: &domain.PrivacyPolicy{ - TOSLink: "TOSLinkChange", - PrivacyLink: "PrivacyLinkChange", - HelpLink: "HelpLinkChange", + TOSLink: "TOSLinkChange", + PrivacyLink: "PrivacyLinkChange", + HelpLink: "HelpLinkChange", + SupportEmail: "support2@example.com", }, }, res: res{ @@ -313,9 +365,10 @@ func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { AggregateID: "org1", ResourceOwner: "org1", }, - TOSLink: "TOSLinkChange", - PrivacyLink: "PrivacyLinkChange", - HelpLink: "HelpLinkChange", + TOSLink: "TOSLinkChange", + PrivacyLink: "PrivacyLinkChange", + HelpLink: "HelpLinkChange", + SupportEmail: "support2@example.com", }, }, }, @@ -331,13 +384,14 @@ func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { "TOSLink", "PrivacyLink", "HelpLink", + "support@example.com", ), ), ), expectPush( []*repository.Event{ eventFromEventPusher( - newPrivacyPolicyChangedEvent(context.Background(), "org1", "", "", ""), + newPrivacyPolicyChangedEvent(context.Background(), "org1", "", "", "", ""), ), }, ), @@ -347,9 +401,10 @@ func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { ctx: context.Background(), orgID: "org1", policy: &domain.PrivacyPolicy{ - TOSLink: "", - PrivacyLink: "", - HelpLink: "", + TOSLink: "", + PrivacyLink: "", + HelpLink: "", + SupportEmail: "", }, }, res: res{ @@ -358,9 +413,10 @@ func TestCommandSide_ChangePrivacyPolicy(t *testing.T) { AggregateID: "org1", ResourceOwner: "org1", }, - TOSLink: "", - PrivacyLink: "", - HelpLink: "", + TOSLink: "", + PrivacyLink: "", + HelpLink: "", + SupportEmail: "", }, }, }, @@ -444,6 +500,7 @@ func TestCommandSide_RemovePrivacyPolicy(t *testing.T) { "TOSLink", "PrivacyLink", "HelpLink", + "support@example.com", ), ), ), @@ -487,13 +544,14 @@ func TestCommandSide_RemovePrivacyPolicy(t *testing.T) { } } -func newPrivacyPolicyChangedEvent(ctx context.Context, orgID string, tosLink, privacyLink, helpLink string) *org.PrivacyPolicyChangedEvent { +func newPrivacyPolicyChangedEvent(ctx context.Context, orgID string, tosLink, privacyLink, helpLink, supportEmail string) *org.PrivacyPolicyChangedEvent { event, _ := org.NewPrivacyPolicyChangedEvent(ctx, &org.NewAggregate(orgID).Aggregate, []policy.PrivacyPolicyChanges{ policy.ChangeTOSLink(tosLink), policy.ChangePrivacyLink(privacyLink), policy.ChangeHelpLink(helpLink), + policy.ChangeSupportEmail(domain.EmailAddress(supportEmail)), }, ) return event diff --git a/internal/command/policy_privacy_model.go b/internal/command/policy_privacy_model.go index 2283ee0cf4..1831176d7a 100644 --- a/internal/command/policy_privacy_model.go +++ b/internal/command/policy_privacy_model.go @@ -9,10 +9,11 @@ import ( type PrivacyPolicyWriteModel struct { eventstore.WriteModel - TOSLink string - PrivacyLink string - HelpLink string - State domain.PolicyState + TOSLink string + PrivacyLink string + HelpLink string + SupportEmail domain.EmailAddress + State domain.PolicyState } func (wm *PrivacyPolicyWriteModel) Reduce() error { @@ -22,6 +23,7 @@ func (wm *PrivacyPolicyWriteModel) Reduce() error { wm.TOSLink = e.TOSLink wm.PrivacyLink = e.PrivacyLink wm.HelpLink = e.HelpLink + wm.SupportEmail = e.SupportEmail wm.State = domain.PolicyStateActive case *policy.PrivacyPolicyChangedEvent: if e.PrivacyLink != nil { @@ -33,6 +35,9 @@ func (wm *PrivacyPolicyWriteModel) Reduce() error { if e.HelpLink != nil { wm.HelpLink = *e.HelpLink } + if e.SupportEmail != nil { + wm.SupportEmail = *e.SupportEmail + } case *policy.PrivacyPolicyRemovedEvent: wm.State = domain.PolicyStateRemoved } diff --git a/internal/domain/custom_login_text.go b/internal/domain/custom_login_text.go index d459dfe643..18d999ce93 100644 --- a/internal/domain/custom_login_text.go +++ b/internal/domain/custom_login_text.go @@ -296,6 +296,7 @@ const ( LoginKeyFooterTOS = LoginKeyFooter + "Tos" LoginKeyFooterPrivacyPolicy = LoginKeyFooter + "PrivacyPolicy" LoginKeyFooterHelp = LoginKeyFooter + "Help" + LoginKeyFooterSupportEmail = LoginKeyFooter + "SupportEmail" ) type CustomLoginText struct { @@ -639,6 +640,7 @@ type FooterText struct { TOS string PrivacyPolicy string Help string + SupportEmail string } type PasswordlessPromptScreenText struct { diff --git a/internal/domain/policy_privacy.go b/internal/domain/policy_privacy.go index b018c73f89..0851582815 100644 --- a/internal/domain/policy_privacy.go +++ b/internal/domain/policy_privacy.go @@ -10,7 +10,8 @@ type PrivacyPolicy struct { State PolicyState Default bool - TOSLink string - PrivacyLink string - HelpLink string + TOSLink string + PrivacyLink string + HelpLink string + SupportEmail EmailAddress } diff --git a/internal/iam/model/privacy_policy_view.go b/internal/iam/model/privacy_policy_view.go index 92d5031d4f..6d40dd3937 100644 --- a/internal/iam/model/privacy_policy_view.go +++ b/internal/iam/model/privacy_policy_view.go @@ -10,6 +10,7 @@ type PrivacyPolicyView struct { AggregateID string TOSLink string PrivacyLink string + SupportEmail string Default bool CreationDate time.Time diff --git a/internal/query/custom_text.go b/internal/query/custom_text.go index dcf9628d2b..415cbe3023 100644 --- a/internal/query/custom_text.go +++ b/internal/query/custom_text.go @@ -1181,4 +1181,7 @@ func footerKeyToDomain(text *CustomText, result *domain.CustomLoginText) { if text.Key == domain.LoginKeyFooterHelp { result.Footer.Help = text.Text } + if text.Key == domain.LoginKeyFooterSupportEmail { + result.Footer.SupportEmail = text.Text + } } diff --git a/internal/query/privacy_policy.go b/internal/query/privacy_policy.go index 0cbfe9f287..2874c6d22a 100644 --- a/internal/query/privacy_policy.go +++ b/internal/query/privacy_policy.go @@ -24,9 +24,10 @@ type PrivacyPolicy struct { ResourceOwner string State domain.PolicyState - TOSLink string - PrivacyLink string - HelpLink string + TOSLink string + PrivacyLink string + HelpLink string + SupportEmail domain.EmailAddress IsDefault bool } @@ -72,6 +73,10 @@ var ( name: projection.PrivacyPolicyHelpLinkCol, table: privacyTable, } + PrivacyColSupportEmail = Column{ + name: projection.PrivacyPolicySupportEmailCol, + table: privacyTable, + } PrivacyColIsDefault = Column{ name: projection.PrivacyPolicyIsDefaultCol, table: privacyTable, @@ -148,6 +153,7 @@ func preparePrivacyPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sele PrivacyColPrivacyLink.identifier(), PrivacyColTOSLink.identifier(), PrivacyColHelpLink.identifier(), + PrivacyColSupportEmail.identifier(), PrivacyColIsDefault.identifier(), PrivacyColState.identifier(), ). @@ -164,6 +170,7 @@ func preparePrivacyPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sele &policy.PrivacyLink, &policy.TOSLink, &policy.HelpLink, + &policy.SupportEmail, &policy.IsDefault, &policy.State, ) @@ -179,9 +186,10 @@ func preparePrivacyPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sele func (p *PrivacyPolicy) ToDomain() *domain.PrivacyPolicy { return &domain.PrivacyPolicy{ - TOSLink: p.TOSLink, - PrivacyLink: p.PrivacyLink, - HelpLink: p.HelpLink, - Default: p.IsDefault, + TOSLink: p.TOSLink, + PrivacyLink: p.PrivacyLink, + HelpLink: p.HelpLink, + SupportEmail: p.SupportEmail, + Default: p.IsDefault, } } diff --git a/internal/query/privacy_policy_test.go b/internal/query/privacy_policy_test.go index b9966c2b7d..ea68af11b2 100644 --- a/internal/query/privacy_policy_test.go +++ b/internal/query/privacy_policy_test.go @@ -13,17 +13,18 @@ import ( ) var ( - preparePrivacyPolicyStmt = `SELECT projections.privacy_policies2.id,` + - ` projections.privacy_policies2.sequence,` + - ` projections.privacy_policies2.creation_date,` + - ` projections.privacy_policies2.change_date,` + - ` projections.privacy_policies2.resource_owner,` + - ` projections.privacy_policies2.privacy_link,` + - ` projections.privacy_policies2.tos_link,` + - ` projections.privacy_policies2.help_link,` + - ` projections.privacy_policies2.is_default,` + - ` projections.privacy_policies2.state` + - ` FROM projections.privacy_policies2` + + preparePrivacyPolicyStmt = `SELECT projections.privacy_policies3.id,` + + ` projections.privacy_policies3.sequence,` + + ` projections.privacy_policies3.creation_date,` + + ` projections.privacy_policies3.change_date,` + + ` projections.privacy_policies3.resource_owner,` + + ` projections.privacy_policies3.privacy_link,` + + ` projections.privacy_policies3.tos_link,` + + ` projections.privacy_policies3.help_link,` + + ` projections.privacy_policies3.support_email,` + + ` projections.privacy_policies3.is_default,` + + ` projections.privacy_policies3.state` + + ` FROM projections.privacy_policies3` + ` AS OF SYSTEM TIME '-1 ms'` preparePrivacyPolicyCols = []string{ "id", @@ -34,6 +35,7 @@ var ( "privacy_link", "tos_link", "help_link", + "support_email", "is_default", "state", } @@ -84,6 +86,7 @@ func Test_PrivacyPolicyPrepares(t *testing.T) { "privacy.ch", "tos.ch", "help.ch", + "support@example.com", true, domain.PolicyStateActive, }, @@ -99,6 +102,7 @@ func Test_PrivacyPolicyPrepares(t *testing.T) { PrivacyLink: "privacy.ch", TOSLink: "tos.ch", HelpLink: "help.ch", + SupportEmail: "support@example.com", IsDefault: true, }, }, diff --git a/internal/query/projection/privacy_policy.go b/internal/query/projection/privacy_policy.go index ca0b8b3bab..525363e42f 100644 --- a/internal/query/projection/privacy_policy.go +++ b/internal/query/projection/privacy_policy.go @@ -14,7 +14,7 @@ import ( ) const ( - PrivacyPolicyTable = "projections.privacy_policies2" + PrivacyPolicyTable = "projections.privacy_policies3" PrivacyPolicyIDCol = "id" PrivacyPolicyCreationDateCol = "creation_date" @@ -27,6 +27,7 @@ const ( PrivacyPolicyPrivacyLinkCol = "privacy_link" PrivacyPolicyTOSLinkCol = "tos_link" PrivacyPolicyHelpLinkCol = "help_link" + PrivacyPolicySupportEmailCol = "support_email" PrivacyPolicyOwnerRemovedCol = "owner_removed" ) @@ -51,6 +52,7 @@ func newPrivacyPolicyProjection(ctx context.Context, config crdb.StatementHandle crdb.NewColumn(PrivacyPolicyPrivacyLinkCol, crdb.ColumnTypeText), crdb.NewColumn(PrivacyPolicyTOSLinkCol, crdb.ColumnTypeText), crdb.NewColumn(PrivacyPolicyHelpLinkCol, crdb.ColumnTypeText), + crdb.NewColumn(PrivacyPolicySupportEmailCol, crdb.ColumnTypeText), crdb.NewColumn(PrivacyPolicyOwnerRemovedCol, crdb.ColumnTypeBool, crdb.Default(false)), }, crdb.NewPrimaryKey(PrivacyPolicyInstanceIDCol, PrivacyPolicyIDCol), @@ -128,6 +130,7 @@ func (p *privacyPolicyProjection) reduceAdded(event eventstore.Event) (*handler. handler.NewCol(PrivacyPolicyPrivacyLinkCol, policyEvent.PrivacyLink), handler.NewCol(PrivacyPolicyTOSLinkCol, policyEvent.TOSLink), handler.NewCol(PrivacyPolicyHelpLinkCol, policyEvent.HelpLink), + handler.NewCol(PrivacyPolicySupportEmailCol, policyEvent.SupportEmail), handler.NewCol(PrivacyPolicyIsDefaultCol, isDefault), handler.NewCol(PrivacyPolicyResourceOwnerCol, policyEvent.Aggregate().ResourceOwner), handler.NewCol(PrivacyPolicyInstanceIDCol, policyEvent.Aggregate().InstanceID), @@ -157,6 +160,9 @@ func (p *privacyPolicyProjection) reduceChanged(event eventstore.Event) (*handle if policyEvent.HelpLink != nil { cols = append(cols, handler.NewCol(PrivacyPolicyHelpLinkCol, *policyEvent.HelpLink)) } + if policyEvent.SupportEmail != nil { + cols = append(cols, handler.NewCol(PrivacyPolicySupportEmailCol, *policyEvent.SupportEmail)) + } return crdb.NewUpdateStatement( &policyEvent, cols, diff --git a/internal/query/projection/privacy_policy_test.go b/internal/query/projection/privacy_policy_test.go index 61866e01b5..f976c64e9b 100644 --- a/internal/query/projection/privacy_policy_test.go +++ b/internal/query/projection/privacy_policy_test.go @@ -31,8 +31,8 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { []byte(`{ "tosLink": "http://tos.link", "privacyLink": "http://privacy.link", - "helpLink": "http://help.link" -}`), + "helpLink": "http://help.link", + "supportEmail": "support@example.com"}`), ), org.PrivacyPolicyAddedEventMapper), }, reduce: (&privacyPolicyProjection{}).reduceAdded, @@ -43,7 +43,7 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.privacy_policies2 (creation_date, change_date, sequence, id, state, privacy_link, tos_link, help_link, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", + expectedStmt: "INSERT INTO projections.privacy_policies3 (creation_date, change_date, sequence, id, state, privacy_link, tos_link, help_link, support_email, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -53,6 +53,7 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { "http://privacy.link", "http://tos.link", "http://help.link", + domain.EmailAddress("support@example.com"), false, "ro-id", "instance-id", @@ -72,8 +73,8 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { []byte(`{ "tosLink": "http://tos.link", "privacyLink": "http://privacy.link", - "helpLink": "http://help.link" - }`), + "helpLink": "http://help.link", + "supportEmail": "support@example.com"}`), ), org.PrivacyPolicyChangedEventMapper), }, want: wantReduce{ @@ -83,13 +84,14 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.privacy_policies2 SET (change_date, sequence, privacy_link, tos_link, help_link) = ($1, $2, $3, $4, $5) WHERE (id = $6) AND (instance_id = $7)", + expectedStmt: "UPDATE projections.privacy_policies3 SET (change_date, sequence, privacy_link, tos_link, help_link, support_email) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ anyArg{}, uint64(15), "http://privacy.link", "http://tos.link", "http://help.link", + domain.EmailAddress("support@example.com"), "agg-id", "instance-id", }, @@ -115,7 +117,7 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.privacy_policies2 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.privacy_policies3 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -141,7 +143,7 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.privacy_policies2 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.privacy_policies3 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, @@ -160,8 +162,8 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { []byte(`{ "tosLink": "http://tos.link", "privacyLink": "http://privacy.link", - "helpLink": "http://help.link" - }`), + "helpLink": "http://help.link", + "supportEmail": "support@example.com"}`), ), instance.PrivacyPolicyAddedEventMapper), }, want: wantReduce{ @@ -171,7 +173,7 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.privacy_policies2 (creation_date, change_date, sequence, id, state, privacy_link, tos_link, help_link, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", + expectedStmt: "INSERT INTO projections.privacy_policies3 (creation_date, change_date, sequence, id, state, privacy_link, tos_link, help_link, support_email, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -181,6 +183,7 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { "http://privacy.link", "http://tos.link", "http://help.link", + domain.EmailAddress("support@example.com"), true, "ro-id", "instance-id", @@ -200,8 +203,8 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { []byte(`{ "tosLink": "http://tos.link", "privacyLink": "http://privacy.link", - "helpLink": "http://help.link" - }`), + "helpLink": "http://help.link", + "supportEmail": "support@example.com"}`), ), instance.PrivacyPolicyChangedEventMapper), }, want: wantReduce{ @@ -211,13 +214,14 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.privacy_policies2 SET (change_date, sequence, privacy_link, tos_link, help_link) = ($1, $2, $3, $4, $5) WHERE (id = $6) AND (instance_id = $7)", + expectedStmt: "UPDATE projections.privacy_policies3 SET (change_date, sequence, privacy_link, tos_link, help_link, support_email) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ anyArg{}, uint64(15), "http://privacy.link", "http://tos.link", "http://help.link", + domain.EmailAddress("support@example.com"), "agg-id", "instance-id", }, @@ -243,7 +247,7 @@ func TestPrivacyPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.privacy_policies2 SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)", + expectedStmt: "UPDATE projections.privacy_policies3 SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), diff --git a/internal/repository/instance/policy_privacy.go b/internal/repository/instance/policy_privacy.go index c629894eea..2851ba784b 100644 --- a/internal/repository/instance/policy_privacy.go +++ b/internal/repository/instance/policy_privacy.go @@ -3,8 +3,8 @@ package instance import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/repository" "github.com/zitadel/zitadel/internal/repository/policy" ) @@ -24,6 +24,7 @@ func NewPrivacyPolicyAddedEvent( tosLink, privacyLink, helpLink string, + supportEmail domain.EmailAddress, ) *PrivacyPolicyAddedEvent { return &PrivacyPolicyAddedEvent{ PrivacyPolicyAddedEvent: *policy.NewPrivacyPolicyAddedEvent( @@ -33,7 +34,8 @@ func NewPrivacyPolicyAddedEvent( PrivacyPolicyAddedEventType), tosLink, privacyLink, - helpLink), + helpLink, + supportEmail), } } diff --git a/internal/repository/org/policy_privacy.go b/internal/repository/org/policy_privacy.go index 78cb7beaa9..380711a3fd 100644 --- a/internal/repository/org/policy_privacy.go +++ b/internal/repository/org/policy_privacy.go @@ -3,8 +3,8 @@ package org import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/repository" "github.com/zitadel/zitadel/internal/repository/policy" ) @@ -25,6 +25,7 @@ func NewPrivacyPolicyAddedEvent( tosLink, privacyLink, helpLink string, + supportEmail domain.EmailAddress, ) *PrivacyPolicyAddedEvent { return &PrivacyPolicyAddedEvent{ PrivacyPolicyAddedEvent: *policy.NewPrivacyPolicyAddedEvent( @@ -34,7 +35,8 @@ func NewPrivacyPolicyAddedEvent( PrivacyPolicyAddedEventType), tosLink, privacyLink, - helpLink), + helpLink, + supportEmail), } } diff --git a/internal/repository/policy/policy_privacy.go b/internal/repository/policy/policy_privacy.go index faccca0924..a168a91cf3 100644 --- a/internal/repository/policy/policy_privacy.go +++ b/internal/repository/policy/policy_privacy.go @@ -3,9 +3,9 @@ package policy import ( "encoding/json" - "github.com/zitadel/zitadel/internal/eventstore" - + "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" ) @@ -18,9 +18,10 @@ const ( type PrivacyPolicyAddedEvent struct { eventstore.BaseEvent `json:"-"` - TOSLink string `json:"tosLink,omitempty"` - PrivacyLink string `json:"privacyLink,omitempty"` - HelpLink string `json:"helpLink,omitempty"` + TOSLink string `json:"tosLink,omitempty"` + PrivacyLink string `json:"privacyLink,omitempty"` + HelpLink string `json:"helpLink,omitempty"` + SupportEmail domain.EmailAddress `json:"supportEmail,omitempty"` } func (e *PrivacyPolicyAddedEvent) Data() interface{} { @@ -36,12 +37,14 @@ func NewPrivacyPolicyAddedEvent( tosLink, privacyLink, helpLink string, + supportEmail domain.EmailAddress, ) *PrivacyPolicyAddedEvent { return &PrivacyPolicyAddedEvent{ - BaseEvent: *base, - TOSLink: tosLink, - PrivacyLink: privacyLink, - HelpLink: helpLink, + BaseEvent: *base, + TOSLink: tosLink, + PrivacyLink: privacyLink, + HelpLink: helpLink, + SupportEmail: supportEmail, } } @@ -60,9 +63,10 @@ func PrivacyPolicyAddedEventMapper(event *repository.Event) (eventstore.Event, e type PrivacyPolicyChangedEvent struct { eventstore.BaseEvent `json:"-"` - TOSLink *string `json:"tosLink,omitempty"` - PrivacyLink *string `json:"privacyLink,omitempty"` - HelpLink *string `json:"helpLink,omitempty"` + TOSLink *string `json:"tosLink,omitempty"` + PrivacyLink *string `json:"privacyLink,omitempty"` + HelpLink *string `json:"helpLink,omitempty"` + SupportEmail *domain.EmailAddress `json:"supportEmail,omitempty"` } func (e *PrivacyPolicyChangedEvent) Data() interface{} { @@ -109,6 +113,12 @@ func ChangeHelpLink(helpLink string) func(*PrivacyPolicyChangedEvent) { } } +func ChangeSupportEmail(supportEmail domain.EmailAddress) func(*PrivacyPolicyChangedEvent) { + return func(e *PrivacyPolicyChangedEvent) { + e.SupportEmail = &supportEmail + } +} + func PrivacyPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) { e := &PrivacyPolicyChangedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index beae4750ec..5bcc5284ad 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -5460,6 +5460,13 @@ message UpdatePrivacyPolicyRequest { example: "\"https://zitadel.com/docs/manuals/introduction\""; } ]; + string support_email = 4 [ + (validate.rules).string = {ignore_empty: true, max_len: 320, email: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"support-email@test.com\""; + description: "help / support email address." + } + ]; } message UpdatePrivacyPolicyResponse { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 73fe75eb4a..71dd9b99ff 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -9834,6 +9834,13 @@ message AddCustomPrivacyPolicyRequest { example: "\"https://zitadel.com/docs/manuals/introduction\""; } ]; + string support_email = 4 [ + (validate.rules).string = {ignore_empty: true, max_len: 320, email: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"support-email@test.com\""; + description: "help / support email address." + } + ]; } message AddCustomPrivacyPolicyResponse { @@ -9859,6 +9866,13 @@ message UpdateCustomPrivacyPolicyRequest { example: "\"https://zitadel.com/docs/manuals/introduction\""; } ]; + string support_email = 4 [ + (validate.rules).string = {ignore_empty: true, max_len: 320, email: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"support-email@test.com\""; + description: "help / support email address." + } + ]; } message UpdateCustomPrivacyPolicyResponse { diff --git a/proto/zitadel/policy.proto b/proto/zitadel/policy.proto index c7d87d092c..c6cbc7a462 100644 --- a/proto/zitadel/policy.proto +++ b/proto/zitadel/policy.proto @@ -4,6 +4,7 @@ import "zitadel/object.proto"; import "zitadel/idp.proto"; import "google/protobuf/duration.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; package zitadel.policy.v1; @@ -345,6 +346,13 @@ message PrivacyPolicy { example: "\"https://zitadel.com/docs/manuals/introduction\""; } ]; + string support_email = 6 [ + (validate.rules).string = {ignore_empty: true, max_len: 320, email: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"support-email@test.com\""; + description: "help / support email address." + } + ]; } message NotificationPolicy { diff --git a/proto/zitadel/text.proto b/proto/zitadel/text.proto index 05689f2aff..602f7f9caf 100644 --- a/proto/zitadel/text.proto +++ b/proto/zitadel/text.proto @@ -394,11 +394,12 @@ message LogoutDoneScreenText { } message FooterText { - reserved 2, 4, 6; + reserved 2, 4, 6, 8; reserved "tos_link", "privacy_policy_link", "help_link"; string tos = 1 [(validate.rules).string = {max_len: 200}]; string privacy_policy = 3 [(validate.rules).string = {max_len: 200}]; string help = 5 [(validate.rules).string = {max_len: 200}]; + string support_email = 7 [(validate.rules).string = {max_len: 200}]; } message PasswordlessPromptScreenText {