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:
Fabi
2021-07-13 07:13:39 +02:00
committed by GitHub
parent 7ebf0333c3
commit 03a38fbf1c
40 changed files with 1108 additions and 245 deletions

View File

@@ -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)
}

View File

@@ -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,
),

View File

@@ -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)

View File

@@ -1,17 +1,18 @@
package eventsourcing
import (
"net/http"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/eventstore/v1"
"net/http"
"golang.org/x/text/language"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/config/types"
es_spol "github.com/caos/zitadel/internal/eventstore/v1/spooler"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/spooler"
noti_view "github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
"golang.org/x/text/language"
)
type Config struct {
@@ -41,11 +42,7 @@ func Start(conf Config, dir http.FileSystem, systemDefaults sd.SystemDefaults, c
return nil, err
}
translator, err := i18n.NewTranslator(dir, i18n.TranslatorConfig{DefaultLanguage: conf.DefaultLanguage})
if err != nil {
return nil, err
}
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, command, systemDefaults, translator, dir, apiDomain)
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, command, systemDefaults, dir, apiDomain)
return &EsRepository{
spool,

View File

@@ -2,13 +2,13 @@ package spooler
import (
"database/sql"
"net/http"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/eventstore/v1"
"net/http"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/handler"
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
)
@@ -20,12 +20,12 @@ type SpoolerConfig struct {
Handlers handler.Configs
}
func StartSpooler(c SpoolerConfig, es v1.Eventstore, view *view.View, sql *sql.DB, command *command.Commands, systemDefaults sd.SystemDefaults, i18n *i18n.Translator, dir http.FileSystem, apiDomain string) *spooler.Spooler {
func StartSpooler(c SpoolerConfig, es v1.Eventstore, view *view.View, sql *sql.DB, command *command.Commands, systemDefaults sd.SystemDefaults, dir http.FileSystem, apiDomain string) *spooler.Spooler {
spoolerConfig := spooler.Config{
Eventstore: es,
Locker: &locker{dbClient: sql},
ConcurrentWorkers: c.ConcurrentWorkers,
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, command, systemDefaults, i18n, dir, apiDomain),
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, command, systemDefaults, dir, apiDomain),
}
spool := spoolerConfig.New()
spool.Start()

View File

@@ -0,0 +1,61 @@
package view
import (
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/iam/repository/view"
"github.com/caos/zitadel/internal/iam/repository/view/model"
global_view "github.com/caos/zitadel/internal/view/repository"
)
const (
customTextTable = "notification.custom_texts"
)
func (v *View) CustomTextByIDs(aggregateID, template, lang, key string) (*model.CustomTextView, error) {
return view.CustomTextByIDs(v.Db, customTextTable, aggregateID, template, lang, key)
}
func (v *View) CustomTextsByAggregateIDAndTemplate(aggregateID, template string) ([]*model.CustomTextView, error) {
return view.GetCustomTextsByAggregateIDAndTemplate(v.Db, customTextTable, aggregateID, template)
}
func (v *View) CustomTextsByAggregateIDAndTemplateAndLang(aggregateID, template, lang string) ([]*model.CustomTextView, error) {
return view.GetCustomTexts(v.Db, customTextTable, aggregateID, template, lang)
}
func (v *View) PutCustomText(template *model.CustomTextView, event *models.Event) error {
err := view.PutCustomText(v.Db, customTextTable, template)
if err != nil {
return err
}
return v.ProcessedCustomTextSequence(event)
}
func (v *View) DeleteCustomText(aggregateID, textType, lang string, event *models.Event) error {
err := view.DeleteCustomText(v.Db, customTextTable, aggregateID, textType, lang)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedCustomTextSequence(event)
}
func (v *View) GetLatestCustomTextSequence() (*global_view.CurrentSequence, error) {
return v.latestSequence(customTextTable)
}
func (v *View) ProcessedCustomTextSequence(event *models.Event) error {
return v.saveCurrentSequence(customTextTable, event)
}
func (v *View) UpdateCustomTextSpoolerRunTimestamp() error {
return v.updateSpoolerRunSequence(customTextTable)
}
func (v *View) GetLatestCustomTextFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(customTextTable, sequence)
}
func (v *View) ProcessedCustomTextFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}