feat: Custom text login (#1925)

* feat: default custom message text

* feat: org custom message text

* feat: org custom message text

* feat: custom messages query side

* feat: default messages

* feat: message text user fields

* feat: check for inactive user

* feat: fix send password reset

* feat: fix custom org text

* feat: add variables to docs

* feat: custom text tests

* feat: fix notifications

* feat: add custom text feature

* feat: add custom text feature

* feat: feature in custom message texts

* feat: add custom text feature in frontend

* feat: merge main

* feat: feature tests

* feat: change phone message in setup

* fix: remove unused code, add event translation

* fix: merge main and fix problems

* fix: english translation file

* fix: migration versions

* fix: setup

* fix: custom login text

* feat: add all possible custom texts for login

* feat: iam login texts

* feat: org login texts

* feat: protos

* fix: custom text in admin api

* fix: add success login text

* fix: docs

* fix: add custom login texts to management api

* fix: add sub messages to custom login texts

* fix: setup custom texts

* feat: get org login texts

* feat: get org login texts

* feat: handler in adminapi

* feat: handlers in auth and admin

* feat: render login texts

* feat: custom login text

* feat: add all login text keys

* feat: handle correct login texts

* feat: custom login texts in command side

* feat: custom login texts in command side

* feat: fix yaml file

* feat: merge master and add confirmation text

* feat: fix html

* feat: read default login texts

* feat: get default text files

* feat: get custom texts org

* feat: tests

* feat: change translator handling

* fix translator from authReq

* feat: change h1 on login screens

* feat: add custom login text for remove

* feat: add custom login text for remove

* feat: cache translation files

* feat: cache translation files

* feat: zitadel user in env var

* feat: add registration user description

* feat: better func naming

* feat: tests

* feat: add mutex to read file

* feat: add mutex to read file

* fix mutex for accessing translation map

* fix: translation key

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Fabi
2021-07-05 15:10:49 +02:00
committed by GitHub
parent 7c0bc8f63d
commit 99b2c33ccb
124 changed files with 21023 additions and 474 deletions

View File

@@ -3,10 +3,15 @@ package eventstore
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"sync"
"time"
"github.com/caos/logging"
"github.com/ghodss/yaml"
"github.com/golang/protobuf/ptypes"
"github.com/caos/zitadel/internal/api/authz"
@@ -31,12 +36,15 @@ import (
)
type OrgRepository struct {
SearchLimit uint64
Eventstore v1.Eventstore
View *mgmt_view.View
Roles []string
SystemDefaults systemdefaults.SystemDefaults
PrefixAvatarURL string
SearchLimit uint64
Eventstore v1.Eventstore
View *mgmt_view.View
Roles []string
SystemDefaults systemdefaults.SystemDefaults
PrefixAvatarURL string
LoginDir http.FileSystem
TranslationFileContents map[string][]byte
mutex sync.Mutex
}
func (repo *OrgRepository) OrgByID(ctx context.Context, id string) (*org_model.OrgView, error) {
@@ -615,6 +623,52 @@ func (repo *OrgRepository) GetMessageText(ctx context.Context, orgID, textType,
return iam_es_model.MessageTextViewToModel(text), err
}
func (repo *OrgRepository) GetDefaultLoginTexts(ctx context.Context, lang string) (*domain.CustomLoginText, error) {
repo.mutex.Lock()
defer repo.mutex.Unlock()
contents, ok := repo.TranslationFileContents[lang]
if !ok {
contents, err := repo.readTranslationFile(fmt.Sprintf("/i18n/%s.yaml", lang))
if err != nil {
return nil, err
}
repo.TranslationFileContents[lang] = contents
}
loginTextMap := make(map[string]interface{})
if err := yaml.Unmarshal(contents, &loginTextMap); err != nil {
return nil, errors.ThrowInternal(err, "TEXT-l0fse", "Errors.TranslationFile.ReadError")
}
texts, err := repo.View.CustomTextsByAggregateIDAndTemplateAndLand(repo.SystemDefaults.IamID, domain.LoginCustomText, lang)
if err != nil {
return nil, err
}
for _, text := range texts {
keys := strings.Split(text.Key, ".")
screenTextMap, ok := loginTextMap[keys[0]].(map[string]interface{})
if !ok {
continue
}
screenTextMap[keys[1]] = text.Text
}
jsonbody, err := json.Marshal(loginTextMap)
if err != nil {
return nil, errors.ThrowInternal(err, "TEXT-2n8fs", "Errors.TranslationFile.MergeError")
}
loginText := new(domain.CustomLoginText)
if err := json.Unmarshal(jsonbody, &loginText); err != nil {
return nil, errors.ThrowInternal(err, "TEXT-2n8fs", "Errors.TranslationFile.MergeError")
}
return loginText, nil
}
func (repo *OrgRepository) GetLoginTexts(ctx context.Context, orgID, lang string) (*domain.CustomLoginText, error) {
texts, err := repo.View.CustomTextsByAggregateIDAndTemplateAndLand(orgID, domain.LoginCustomText, lang)
if err != nil {
return nil, err
}
return iam_es_model.CustomTextViewsToLoginDomain(repo.SystemDefaults.IamID, lang, texts), err
}
func (repo *OrgRepository) getOrgChanges(ctx context.Context, orgID string, lastSequence uint64, limit uint64, sortAscending bool, auditLogRetention time.Duration) (*org_model.OrgChanges, error) {
query := org_view.ChangesQuery(orgID, lastSequence, limit, sortAscending, auditLogRetention)
@@ -710,3 +764,15 @@ func (repo *OrgRepository) getIAMEvents(ctx context.Context, sequence uint64) ([
}
return repo.Eventstore.FilterEvents(ctx, query)
}
func (repo *OrgRepository) readTranslationFile(filename string) ([]byte, error) {
r, err := repo.LoginDir.Open(filename)
if err != nil {
return nil, errors.ThrowInternal(err, "TEXT-3n8fs", "Errors.TranslationFile.ReadError")
}
contents, err := ioutil.ReadAll(r)
if err != nil {
return nil, errors.ThrowInternal(err, "TEXT-322fs", "Errors.TranslationFile.ReadError")
}
return contents, nil
}

View File

@@ -0,0 +1,121 @@
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 = "management.custom_texts"
)
func (m *CustomText) ViewModel() string {
return customTextTable
}
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

@@ -83,6 +83,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
handler{view, bulkLimit, configs.cycleDuration("Features"), errorCount, es}),
newPrivacyPolicy(
handler{view, bulkLimit, configs.cycleDuration("PrivacyPolicy"), errorCount, es}),
newCustomText(
handler{view, bulkLimit, configs.cycleDuration("CustomText"), errorCount, es}),
}
}

View File

@@ -120,7 +120,7 @@ func (m *LabelPolicy) processLabelPolicy(event *es_models.Event) (err error) {
}
func (m *LabelPolicy) OnError(event *es_models.Event, err error) error {
logging.LogWithFields("SPOOL-4Djo9", "id", event.AggregateID).WithError(err).Warn("something went wrong in label policy handler")
logging.LogWithFields("SPOOL-66Cs8", "id", event.AggregateID).WithError(err).Warn("something went wrong in label policy handler")
return spooler.HandleError(event, err, m.view.GetLatestLabelPolicyFailedEvent, m.view.ProcessedLabelPolicyFailedEvent, m.view.ProcessedLabelPolicySequence, m.errorCountUntilSkip)
}

View File

@@ -132,7 +132,7 @@ func (m *LoginPolicy) processLoginPolicy(event *es_models.Event) (err error) {
}
func (m *LoginPolicy) OnError(event *es_models.Event, err error) error {
logging.LogWithFields("SPOOL-4Djo9", "id", event.AggregateID).WithError(err).Warn("something went wrong in login policy handler")
logging.LogWithFields("SPOOL-92n8F", "id", event.AggregateID).WithError(err).Warn("something went wrong in login policy handler")
return spooler.HandleError(event, err, m.view.GetLatestLoginPolicyFailedEvent, m.view.ProcessedLoginPolicyFailedEvent, m.view.ProcessedLoginPolicySequence, m.errorCountUntilSkip)
}

View File

@@ -97,7 +97,7 @@ func (m *MailTemplate) processMailTemplate(event *es_models.Event) (err error) {
}
func (m *MailTemplate) OnError(event *es_models.Event, err error) error {
logging.LogWithFields("SPOOL-4Djo9", "id", event.AggregateID).WithError(err).Warn("something went wrong in label template handler")
logging.LogWithFields("SPOOL-1n87f", "id", event.AggregateID).WithError(err).Warn("something went wrong in label template handler")
return spooler.HandleError(event, err, m.view.GetLatestMailTemplateFailedEvent, m.view.ProcessedMailTemplateFailedEvent, m.view.ProcessedMailTemplateSequence, m.errorCountUntilSkip)
}

View File

@@ -79,30 +79,36 @@ func (m *MessageText) processMessageText(event *es_models.Event) (err error) {
switch event.Type {
case iam_es_model.CustomTextSet, model.CustomTextSet,
iam_es_model.CustomTextRemoved, model.CustomTextRemoved:
text := new(iam_model.CustomText)
text := new(iam_model.CustomTextView)
err = text.SetData(event)
if err != nil {
return err
}
message, err = m.view.MessageTextByIDs(event.AggregateID, text.Template, text.Language.String())
if !text.IsMessageTemplate() {
return m.view.ProcessedMessageTextSequence(event)
}
message, err = m.view.MessageTextByIDs(event.AggregateID, text.Template, text.Language)
if err != nil && !caos_errs.IsNotFound(err) {
return err
}
if caos_errs.IsNotFound(err) {
err = nil
message = new(iam_model.MessageTextView)
message.Language = text.Language.String()
message.Language = text.Language
message.MessageTextType = text.Template
message.CreationDate = event.CreationDate
}
err = message.AppendEvent(event)
case model.CustomTextMessageRemoved:
text := new(iam_model.CustomText)
text := new(iam_model.CustomTextView)
err = text.SetData(event)
if err != nil {
return err
}
return m.view.DeleteMessageText(event.AggregateID, text.Template, text.Language.String(), event)
if !text.IsMessageTemplate() {
return m.view.ProcessedMessageTextSequence(event)
}
return m.view.DeleteMessageText(event.AggregateID, text.Template, text.Language, event)
default:
return m.view.ProcessedMessageTextSequence(event)
}
@@ -113,7 +119,7 @@ func (m *MessageText) processMessageText(event *es_models.Event) (err error) {
}
func (m *MessageText) OnError(event *es_models.Event, err error) error {
logging.LogWithFields("SPOOL-4Djo9", "id", event.AggregateID).WithError(err).Warn("something went wrong in label text handler")
logging.LogWithFields("SPOOL-om8Hu", "id", event.AggregateID).WithError(err).Warn("something went wrong in label text handler")
return spooler.HandleError(event, err, m.view.GetLatestMessageTextFailedEvent, m.view.ProcessedMessageTextFailedEvent, m.view.ProcessedMessageTextSequence, m.errorCountUntilSkip)
}

View File

@@ -1,6 +1,9 @@
package eventsourcing
import (
"github.com/caos/logging"
"github.com/rakyll/statik/fs"
"github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/internal/static"
@@ -52,9 +55,21 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, roles []string, querie
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, systemDefaults, staticStorage)
assetsAPI := conf.APIDomain + "/assets/v1/"
statikLoginFS, err := fs.NewWithNamespace("login")
logging.Log("CONFI-7usEW").OnError(err).Panic("unable to start login statik dir")
return &EsRepository{
spooler: spool,
OrgRepository: eventstore.OrgRepository{conf.SearchLimit, es, view, roles, systemDefaults, assetsAPI},
spooler: spool,
OrgRepository: eventstore.OrgRepository{
SearchLimit: conf.SearchLimit,
Eventstore: es,
View: view,
Roles: roles,
SystemDefaults: systemDefaults,
PrefixAvatarURL: assetsAPI,
LoginDir: statikLoginFS,
TranslationFileContents: make(map[string][]byte),
},
ProjectRepo: eventstore.ProjectRepo{es, conf.SearchLimit, view, roles, systemDefaults.IamID, assetsAPI},
UserRepo: eventstore.UserRepo{es, conf.SearchLimit, view, systemDefaults, assetsAPI},
UserGrantRepo: eventstore.UserGrantRepo{conf.SearchLimit, view, assetsAPI},

View File

@@ -0,0 +1,57 @@
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 = "management.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) CustomTextsByAggregateIDAndTemplateAndLand(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)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/caos/zitadel/internal/domain"
iam_model "github.com/caos/zitadel/internal/iam/model"
org_model "github.com/caos/zitadel/internal/org/model"
@@ -52,6 +53,9 @@ type OrgRepository interface {
GetDefaultMessageText(ctx context.Context, textType string, language string) (*iam_model.MessageTextView, error)
GetMessageText(ctx context.Context, orgID, textType, language string) (*iam_model.MessageTextView, error)
GetDefaultLoginTexts(ctx context.Context, lang string) (*domain.CustomLoginText, error)
GetLoginTexts(ctx context.Context, orgID, lang string) (*domain.CustomLoginText, error)
GetLabelPolicy(ctx context.Context) (*iam_model.LabelPolicyView, error)
GetPreviewLabelPolicy(ctx context.Context) (*iam_model.LabelPolicyView, error)
GetDefaultLabelPolicy(ctx context.Context) (*iam_model.LabelPolicyView, error)