mirror of
https://github.com/zitadel/zitadel.git
synced 2025-10-26 23:51:42 +00:00
feat: Custom message files (#1992)
* feat: add get custom message text to admin api * feat: read custom message texts from files * feat: get languages in apis * feat: get languages in apis * feat: get languages in apis * feat: pr feedback * feat: docs * feat: merge main
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
|
||||
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
|
||||
iam_model "github.com/caos/zitadel/internal/iam/repository/view/model"
|
||||
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
type CustomText struct {
|
||||
handler
|
||||
subscription *v1.Subscription
|
||||
}
|
||||
|
||||
func newCustomText(handler handler) *CustomText {
|
||||
h := &CustomText{
|
||||
handler: handler,
|
||||
}
|
||||
|
||||
h.subscribe()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (m *CustomText) subscribe() {
|
||||
m.subscription = m.es.Subscribe(m.AggregateTypes()...)
|
||||
go func() {
|
||||
for event := range m.subscription.Events {
|
||||
query.ReduceEvent(m, event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
const (
|
||||
customTextTable = "notification.custom_texts"
|
||||
)
|
||||
|
||||
func (m *CustomText) ViewModel() string {
|
||||
return customTextTable
|
||||
}
|
||||
|
||||
func (m *CustomText) Subscription() *v1.Subscription {
|
||||
return m.subscription
|
||||
}
|
||||
|
||||
func (_ *CustomText) AggregateTypes() []es_models.AggregateType {
|
||||
return []es_models.AggregateType{model.OrgAggregate, iam_es_model.IAMAggregate}
|
||||
}
|
||||
|
||||
func (p *CustomText) CurrentSequence() (uint64, error) {
|
||||
sequence, err := p.view.GetLatestCustomTextSequence()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (m *CustomText) EventQuery() (*es_models.SearchQuery, error) {
|
||||
sequence, err := m.view.GetLatestCustomTextSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(m.AggregateTypes()...).
|
||||
LatestSequenceFilter(sequence.CurrentSequence), nil
|
||||
}
|
||||
|
||||
func (m *CustomText) Reduce(event *es_models.Event) (err error) {
|
||||
switch event.AggregateType {
|
||||
case model.OrgAggregate, iam_es_model.IAMAggregate:
|
||||
err = m.processCustomText(event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *CustomText) processCustomText(event *es_models.Event) (err error) {
|
||||
customText := new(iam_model.CustomTextView)
|
||||
switch event.Type {
|
||||
case iam_es_model.CustomTextSet, model.CustomTextSet:
|
||||
text := new(iam_model.CustomTextView)
|
||||
err = text.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
customText, err = m.view.CustomTextByIDs(event.AggregateID, text.Template, text.Key, text.Language)
|
||||
if err != nil && !caos_errs.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
if caos_errs.IsNotFound(err) {
|
||||
err = nil
|
||||
customText = new(iam_model.CustomTextView)
|
||||
customText.Language = text.Language
|
||||
customText.Template = text.Template
|
||||
customText.CreationDate = event.CreationDate
|
||||
}
|
||||
err = customText.AppendEvent(event)
|
||||
case iam_es_model.CustomTextRemoved, model.CustomTextRemoved:
|
||||
text := new(iam_model.CustomTextView)
|
||||
err = text.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.view.DeleteCustomText(event.AggregateID, text.Template, text.Language, event)
|
||||
default:
|
||||
return m.view.ProcessedCustomTextSequence(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.view.PutCustomText(customText, event)
|
||||
}
|
||||
|
||||
func (m *CustomText) OnError(event *es_models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-3m912", "id", event.AggregateID).WithError(err).Warn("something went wrong in custom text handler")
|
||||
return spooler.HandleError(event, err, m.view.GetLatestCustomTextFailedEvent, m.view.ProcessedCustomTextFailedEvent, m.view.ProcessedCustomTextSequence, m.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func (o *CustomText) OnSuccess() error {
|
||||
return spooler.HandleSuccess(o.view.UpdateCustomTextSpoolerRunTimestamp)
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/command"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/command"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/caos/zitadel/internal/i18n"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
|
||||
)
|
||||
|
||||
@@ -34,7 +35,7 @@ func (h *handler) Eventstore() v1.Eventstore {
|
||||
return h.es
|
||||
}
|
||||
|
||||
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es v1.Eventstore, command *command.Commands, systemDefaults sd.SystemDefaults, i18n *i18n.Translator, dir http.FileSystem, apiDomain string) []query.Handler {
|
||||
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es v1.Eventstore, command *command.Commands, systemDefaults sd.SystemDefaults, dir http.FileSystem, apiDomain string) []query.Handler {
|
||||
aesCrypto, err := crypto.NewAESCrypto(systemDefaults.UserVerificationKey)
|
||||
if err != nil {
|
||||
logging.Log("HANDL-s90ew").WithError(err).Debug("error create new aes crypto")
|
||||
@@ -44,12 +45,12 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
|
||||
handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es},
|
||||
systemDefaults.IamID,
|
||||
),
|
||||
newCustomText(handler{view, bulkLimit, configs.cycleDuration("CustomText"), errorCount, es}),
|
||||
newNotification(
|
||||
handler{view, bulkLimit, configs.cycleDuration("Notification"), errorCount, es},
|
||||
command,
|
||||
systemDefaults,
|
||||
aesCrypto,
|
||||
i18n,
|
||||
dir,
|
||||
apiDomain,
|
||||
),
|
||||
|
||||
@@ -29,19 +29,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
notificationTable = "notification.notifications"
|
||||
NotifyUserID = "NOTIFICATION"
|
||||
labelPolicyTableOrg = "management.label_policies"
|
||||
labelPolicyTableDef = "adminapi.label_policies"
|
||||
mailTemplateTableOrg = "management.mail_templates"
|
||||
mailTemplateTableDef = "adminapi.mail_templates"
|
||||
messageTextTableOrg = "management.message_texts"
|
||||
messageTextTableDef = "adminapi.message_texts"
|
||||
messageTextTypeDomainClaimed = "DomainClaimed"
|
||||
messageTextTypeInitCode = "InitCode"
|
||||
messageTextTypePasswordReset = "PasswordReset"
|
||||
messageTextTypeVerifyEmail = "VerifyEmail"
|
||||
messageTextTypeVerifyPhone = "VerifyPhone"
|
||||
notificationTable = "notification.notifications"
|
||||
NotifyUserID = "NOTIFICATION"
|
||||
labelPolicyTableOrg = "management.label_policies"
|
||||
labelPolicyTableDef = "adminapi.label_policies"
|
||||
mailTemplateTableOrg = "management.mail_templates"
|
||||
mailTemplateTableDef = "adminapi.mail_templates"
|
||||
messageTextTableOrg = "management.message_texts"
|
||||
messageTextTableDef = "adminapi.message_texts"
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
@@ -49,7 +44,6 @@ type Notification struct {
|
||||
command *command.Commands
|
||||
systemDefaults sd.SystemDefaults
|
||||
AesCrypto crypto.EncryptionAlgorithm
|
||||
i18n *i18n.Translator
|
||||
statikDir http.FileSystem
|
||||
subscription *v1.Subscription
|
||||
apiDomain string
|
||||
@@ -60,7 +54,6 @@ func newNotification(
|
||||
command *command.Commands,
|
||||
defaults sd.SystemDefaults,
|
||||
aesCrypto crypto.EncryptionAlgorithm,
|
||||
translator *i18n.Translator,
|
||||
statikDir http.FileSystem,
|
||||
apiDomain string,
|
||||
) *Notification {
|
||||
@@ -68,7 +61,6 @@ func newNotification(
|
||||
handler: handler,
|
||||
command: command,
|
||||
systemDefaults: defaults,
|
||||
i18n: translator,
|
||||
statikDir: statikDir,
|
||||
AesCrypto: aesCrypto,
|
||||
apiDomain: apiDomain,
|
||||
@@ -166,12 +158,12 @@ func (n *Notification) handleInitUserCode(event *models.Event) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
text, err := n.getMessageText(user, messageTextTypeInitCode, user.PreferredLanguage)
|
||||
translator, err := n.getTranslatorWithOrgTexts(user.ResourceOwner, domain.InitCodeMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = types.SendUserInitCode(string(template.Template), text, user, initCode, n.systemDefaults, n.AesCrypto, colors, n.apiDomain)
|
||||
err = types.SendUserInitCode(string(template.Template), translator, user, initCode, n.systemDefaults, n.AesCrypto, colors, n.apiDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -205,11 +197,11 @@ func (n *Notification) handlePasswordCode(event *models.Event) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
text, err := n.getMessageText(user, messageTextTypePasswordReset, user.PreferredLanguage)
|
||||
translator, err := n.getTranslatorWithOrgTexts(user.ResourceOwner, domain.PasswordResetMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendPasswordCode(string(template.Template), text, user, pwCode, n.systemDefaults, n.AesCrypto, colors, n.apiDomain)
|
||||
err = types.SendPasswordCode(string(template.Template), translator, user, pwCode, n.systemDefaults, n.AesCrypto, colors, n.apiDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -243,12 +235,12 @@ func (n *Notification) handleEmailVerificationCode(event *models.Event) (err err
|
||||
return err
|
||||
}
|
||||
|
||||
text, err := n.getMessageText(user, messageTextTypeVerifyEmail, user.PreferredLanguage)
|
||||
translator, err := n.getTranslatorWithOrgTexts(user.ResourceOwner, domain.VerifyEmailMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = types.SendEmailVerificationCode(string(template.Template), text, user, emailCode, n.systemDefaults, n.AesCrypto, colors, n.apiDomain)
|
||||
err = types.SendEmailVerificationCode(string(template.Template), translator, user, emailCode, n.systemDefaults, n.AesCrypto, colors, n.apiDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -270,11 +262,11 @@ func (n *Notification) handlePhoneVerificationCode(event *models.Event) (err err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
text, err := n.getMessageText(user, messageTextTypeVerifyPhone, user.PreferredLanguage)
|
||||
translator, err := n.getTranslatorWithOrgTexts(user.ResourceOwner, domain.VerifyPhoneMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendPhoneVerificationCode(text, user, phoneCode, n.systemDefaults, n.AesCrypto)
|
||||
err = types.SendPhoneVerificationCode(translator, user, phoneCode, n.systemDefaults, n.AesCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -309,11 +301,11 @@ func (n *Notification) handleDomainClaimed(event *models.Event) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
text, err := n.getMessageText(user, messageTextTypeDomainClaimed, user.PreferredLanguage)
|
||||
translator, err := n.getTranslatorWithOrgTexts(user.ResourceOwner, domain.DomainClaimedMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendDomainClaimed(string(template.Template), text, user, data["userName"], n.systemDefaults, colors, n.apiDomain)
|
||||
err = types.SendDomainClaimed(string(template.Template), translator, user, data["userName"], n.systemDefaults, colors, n.apiDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -400,6 +392,31 @@ func (n *Notification) getMailTemplate(ctx context.Context) (*iam_model.MailTemp
|
||||
return iam_es_model.MailTemplateViewToModel(template), err
|
||||
}
|
||||
|
||||
func (n *Notification) getTranslatorWithOrgTexts(orgID, textType string) (*i18n.Translator, error) {
|
||||
translator, err := i18n.NewTranslator(n.statikDir, i18n.TranslatorConfig{DefaultLanguage: n.systemDefaults.DefaultLanguage})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allCustomTexts, err := n.view.CustomTextsByAggregateIDAndTemplate(domain.IAMID, textType)
|
||||
if err == nil {
|
||||
return translator, nil
|
||||
}
|
||||
customTexts, err := n.view.CustomTextsByAggregateIDAndTemplate(orgID, textType)
|
||||
if err == nil {
|
||||
return translator, nil
|
||||
}
|
||||
allCustomTexts = append(allCustomTexts, customTexts...)
|
||||
|
||||
for _, text := range allCustomTexts {
|
||||
msg := i18n.Message{
|
||||
ID: text.Key,
|
||||
Text: text.Text,
|
||||
}
|
||||
translator.AddMessages(language.Make(text.Language), msg)
|
||||
}
|
||||
return translator, nil
|
||||
}
|
||||
|
||||
// Read organization specific texts
|
||||
func (n *Notification) getMessageText(user *model.NotifyUser, textType, lang string) (*iam_model.MessageTextView, error) {
|
||||
langTag := language.Make(lang)
|
||||
|
||||
Reference in New Issue
Block a user