From 03a38fbf1c59c6e77cd557f79c38baeddaaff850 Mon Sep 17 00:00:00 2001
From: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Date: Tue, 13 Jul 2021 07:13:39 +0200
Subject: [PATCH] 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
---
docs/docs/apis/proto/admin.md | 207 +++++++++++++++++-
docs/docs/apis/proto/auth.md | 29 +++
docs/docs/apis/proto/management.md | 29 +++
.../eventsourcing/eventstore/iam.go | 80 +++++--
.../repository/eventsourcing/repository.go | 21 +-
internal/admin/repository/iam.go | 9 +-
internal/api/grpc/admin/custom_text.go | 60 ++++-
internal/api/grpc/admin/language.go | 16 ++
internal/api/grpc/auth/language.go | 16 ++
internal/api/grpc/management/custom_text.go | 20 +-
internal/api/grpc/management/language.go | 16 ++
.../api/grpc/management/user_converter.go | 2 +-
internal/api/grpc/text/custom_text.go | 3 +-
internal/api/grpc/text/language.go | 13 ++
.../eventsourcing/eventstore/iam.go | 22 +-
.../repository/eventsourcing/repository.go | 7 +
internal/auth/repository/iam.go | 3 +
..._messge_text.go => custom_message_text.go} | 24 ++
internal/iam/model/message_text_view.go | 4 -
.../iam/repository/view/model/custom_text.go | 40 ++++
.../iam/repository/view/model/message_text.go | 6 -
.../eventsourcing/eventstore/org.go | 121 +++++-----
.../repository/eventsourcing/repository.go | 21 +-
internal/management/repository/org.go | 9 +-
.../eventsourcing/handler/custom_text.go | 125 +++++++++++
.../eventsourcing/handler/handler.go | 11 +-
.../eventsourcing/handler/notification.go | 69 +++---
.../repository/eventsourcing/repository.go | 13 +-
.../eventsourcing/spooler/spooler.go | 8 +-
.../eventsourcing/view/custom_texts.go | 61 ++++++
.../notification/templates/templateData.go | 33 +--
internal/notification/types/domain_claimed.go | 17 +-
.../types/email_verification_code.go | 14 +-
internal/notification/types/init_code.go | 18 +-
internal/notification/types/password_code.go | 18 +-
.../types/phone_verification_code.go | 11 +-
migrations/cockroach/V1.55__custom_text.sql | 14 ++
proto/zitadel/admin.proto | 126 ++++++++++-
proto/zitadel/auth.proto | 19 ++
proto/zitadel/management.proto | 18 ++
40 files changed, 1108 insertions(+), 245 deletions(-)
create mode 100644 internal/api/grpc/admin/language.go
create mode 100644 internal/api/grpc/auth/language.go
create mode 100644 internal/api/grpc/management/language.go
create mode 100644 internal/api/grpc/text/language.go
rename internal/domain/{custom_messge_text.go => custom_message_text.go} (64%)
create mode 100644 internal/notification/repository/eventsourcing/handler/custom_text.go
create mode 100644 internal/notification/repository/eventsourcing/view/custom_texts.go
create mode 100644 migrations/cockroach/V1.55__custom_text.sql
diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md
index e725114422..59b19c0199 100644
--- a/docs/docs/apis/proto/admin.md
+++ b/docs/docs/apis/proto/admin.md
@@ -20,6 +20,18 @@ It respondes as soon as ZITADEL started
GET: /healthz
+### GetSupportedLanguages
+
+> **rpc** GetSupportedLanguages([GetSupportedLanguagesRequest](#getsupportedlanguagesrequest))
+[GetSupportedLanguagesResponse](#getsupportedlanguagesresponse)
+
+Returns the default languages
+
+
+
+ GET: /languages
+
+
### IsOrgUnique
> **rpc** IsOrgUnique([IsOrgUniqueRequest](#isorguniquerequest))
@@ -658,7 +670,19 @@ it impacts all organisations without a customised policy
> **rpc** GetDefaultInitMessageText([GetDefaultInitMessageTextRequest](#getdefaultinitmessagetextrequest))
[GetDefaultInitMessageTextResponse](#getdefaultinitmessagetextresponse)
-Returns the custom text for initial message
+Returns the default text for initial message (translation file)
+
+
+
+ GET: /text/default/message/init/{language}
+
+
+### GetCustomInitMessageText
+
+> **rpc** GetCustomInitMessageText([GetCustomInitMessageTextRequest](#getcustominitmessagetextrequest))
+[GetCustomInitMessageTextResponse](#getcustominitmessagetextresponse)
+
+Returns the custom text for initial message (overwritten in eventstore)
@@ -685,7 +709,19 @@ The Following Variables can be used:
> **rpc** GetDefaultPasswordResetMessageText([GetDefaultPasswordResetMessageTextRequest](#getdefaultpasswordresetmessagetextrequest))
[GetDefaultPasswordResetMessageTextResponse](#getdefaultpasswordresetmessagetextresponse)
-Returns the custom text for password reset message
+Returns the default text for password reset message (translation file)
+
+
+
+ GET: /text/deafult/message/passwordreset/{language}
+
+
+### GetCustomPasswordResetMessageText
+
+> **rpc** GetCustomPasswordResetMessageText([GetCustomPasswordResetMessageTextRequest](#getcustompasswordresetmessagetextrequest))
+[GetCustomPasswordResetMessageTextResponse](#getcustompasswordresetmessagetextresponse)
+
+Returns the custom text for password reset message (overwritten in eventstore)
@@ -712,7 +748,19 @@ The Following Variables can be used:
> **rpc** GetDefaultVerifyEmailMessageText([GetDefaultVerifyEmailMessageTextRequest](#getdefaultverifyemailmessagetextrequest))
[GetDefaultVerifyEmailMessageTextResponse](#getdefaultverifyemailmessagetextresponse)
-Returns the custom text for verify email message
+Returns the default text for verify email message (translation files)
+
+
+
+ GET: /text/default/message/verifyemail/{language}
+
+
+### GetCustomVerifyEmailMessageText
+
+> **rpc** GetCustomVerifyEmailMessageText([GetCustomVerifyEmailMessageTextRequest](#getcustomverifyemailmessagetextrequest))
+[GetCustomVerifyEmailMessageTextResponse](#getcustomverifyemailmessagetextresponse)
+
+Returns the custom text for verify email message (overwritten in eventstore)
@@ -739,6 +787,18 @@ The Following Variables can be used:
> **rpc** GetDefaultVerifyPhoneMessageText([GetDefaultVerifyPhoneMessageTextRequest](#getdefaultverifyphonemessagetextrequest))
[GetDefaultVerifyPhoneMessageTextResponse](#getdefaultverifyphonemessagetextresponse)
+Returns the default text for verify phone message (translation file)
+
+
+
+ GET: /text/default/message/verifyphone/{language}
+
+
+### GetCustomVerifyPhoneMessageText
+
+> **rpc** GetCustomVerifyPhoneMessageText([GetCustomVerifyPhoneMessageTextRequest](#getcustomverifyphonemessagetextrequest))
+[GetCustomVerifyPhoneMessageTextResponse](#getcustomverifyphonemessagetextresponse)
+
Returns the custom text for verify phone message
@@ -766,7 +826,19 @@ The Following Variables can be used:
> **rpc** GetDefaultDomainClaimedMessageText([GetDefaultDomainClaimedMessageTextRequest](#getdefaultdomainclaimedmessagetextrequest))
[GetDefaultDomainClaimedMessageTextResponse](#getdefaultdomainclaimedmessagetextresponse)
-Returns the custom text for domain claimed message
+Returns the default text for domain claimed message (translation file)
+
+
+
+ GET: /text/default/message/domainclaimed/{language}
+
+
+### GetCustomDomainClaimedMessageText
+
+> **rpc** GetCustomDomainClaimedMessageText([GetCustomDomainClaimedMessageTextRequest](#getcustomdomainclaimedmessagetextrequest))
+[GetCustomDomainClaimedMessageTextResponse](#getcustomdomainclaimedmessagetextresponse)
+
+Returns the custom text for domain claimed message (overwritten in eventstore)
@@ -1171,6 +1243,50 @@ This is an empty response
+### GetCustomDomainClaimedMessageTextRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| language | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### GetCustomDomainClaimedMessageTextResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| custom_text | zitadel.text.v1.MessageCustomText | - | |
+
+
+
+
+### GetCustomInitMessageTextRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| language | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### GetCustomInitMessageTextResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| custom_text | zitadel.text.v1.MessageCustomText | - | |
+
+
+
+
### GetCustomLoginTextsRequest
@@ -1216,6 +1332,72 @@ This is an empty response
+### GetCustomPasswordResetMessageTextRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| language | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### GetCustomPasswordResetMessageTextResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| custom_text | zitadel.text.v1.MessageCustomText | - | |
+
+
+
+
+### GetCustomVerifyEmailMessageTextRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| language | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### GetCustomVerifyEmailMessageTextResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| custom_text | zitadel.text.v1.MessageCustomText | - | |
+
+
+
+
+### GetCustomVerifyPhoneMessageTextRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| language | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### GetCustomVerifyPhoneMessageTextResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| custom_text | zitadel.text.v1.MessageCustomText | - | |
+
+
+
+
### GetDefaultDomainClaimedMessageTextRequest
@@ -1567,6 +1749,23 @@ This is an empty request
+### GetSupportedLanguagesRequest
+This is an empty request
+
+
+
+
+### GetSupportedLanguagesResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| languages | repeated string | - | |
+
+
+
+
### HealthzRequest
This is an empty request
diff --git a/docs/docs/apis/proto/auth.md b/docs/docs/apis/proto/auth.md
index df1d7b210c..53467b240c 100644
--- a/docs/docs/apis/proto/auth.md
+++ b/docs/docs/apis/proto/auth.md
@@ -19,6 +19,18 @@ title: zitadel/auth.proto
GET: /healthz
+### GetSupportedLanguages
+
+> **rpc** GetSupportedLanguages([GetSupportedLanguagesRequest](#getsupportedlanguagesrequest))
+[GetSupportedLanguagesResponse](#getsupportedlanguagesresponse)
+
+Returns the default languages
+
+
+
+ GET: /languages
+
+
### GetMyUser
> **rpc** GetMyUser([GetMyUserRequest](#getmyuserrequest))
@@ -646,6 +658,23 @@ the request parameters are read from the token-header
+### GetSupportedLanguagesRequest
+This is an empty request
+
+
+
+
+### GetSupportedLanguagesResponse
+This is an empty response
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| languages | repeated string | - | |
+
+
+
+
### HealthzRequest
This is an empty request
diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md
index 8df950489b..e314bca3ce 100644
--- a/docs/docs/apis/proto/management.md
+++ b/docs/docs/apis/proto/management.md
@@ -43,6 +43,18 @@ Returns some needed settings of the IAM (Global Organisation ID, Zitadel Project
GET: /iam
+### GetSupportedLanguages
+
+> **rpc** GetSupportedLanguages([GetSupportedLanguagesRequest](#getsupportedlanguagesrequest))
+[GetSupportedLanguagesResponse](#getsupportedlanguagesresponse)
+
+Returns the default languages
+
+
+
+ GET: /languages
+
+
### GetUserByID
> **rpc** GetUserByID([GetUserByIDRequest](#getuserbyidrequest))
@@ -4282,6 +4294,23 @@ This is an empty request
+### GetSupportedLanguagesRequest
+This is an empty request
+
+
+
+
+### GetSupportedLanguagesResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| languages | repeated string | - | |
+
+
+
+
### GetUserByIDRequest
diff --git a/internal/admin/repository/eventsourcing/eventstore/iam.go b/internal/admin/repository/eventsourcing/eventstore/iam.go
index cd12615849..bc30343d34 100644
--- a/internal/admin/repository/eventsourcing/eventstore/iam.go
+++ b/internal/admin/repository/eventsourcing/eventstore/iam.go
@@ -10,10 +10,12 @@ import (
"sync"
"github.com/ghodss/yaml"
+ "golang.org/x/text/language"
"github.com/caos/zitadel/internal/domain"
v1 "github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/eventstore/v1/models"
+ "github.com/caos/zitadel/internal/i18n"
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
"github.com/caos/zitadel/internal/user/repository/view/model"
@@ -30,15 +32,30 @@ import (
)
type IAMRepository struct {
- Eventstore v1.Eventstore
- SearchLimit uint64
- View *admin_view.View
- SystemDefaults systemdefaults.SystemDefaults
- Roles []string
- PrefixAvatarURL string
- LoginDir http.FileSystem
- TranslationFileContents map[string][]byte
- mutex sync.Mutex
+ Eventstore v1.Eventstore
+ SearchLimit uint64
+ View *admin_view.View
+ SystemDefaults systemdefaults.SystemDefaults
+ Roles []string
+ PrefixAvatarURL string
+ LoginDir http.FileSystem
+ NotificationDir http.FileSystem
+ LoginTranslationFileContents map[string][]byte
+ NotificationTranslationFileContents map[string][]byte
+ mutex sync.Mutex
+ supportedLangs []language.Tag
+}
+
+func (repo *IAMRepository) Languages(ctx context.Context) ([]language.Tag, error) {
+ if len(repo.supportedLangs) == 0 {
+ langs, err := i18n.SupportedLanguages(repo.LoginDir)
+ if err != nil {
+ logging.Log("ADMIN-tiMWs").WithError(err).Debug("unable to parse language")
+ return nil, err
+ }
+ repo.supportedLangs = langs
+ }
+ return repo.supportedLangs, nil
}
func (repo *IAMRepository) IAMMemberByID(ctx context.Context, iamID, userID string) (*iam_model.IAMMemberView, error) {
@@ -365,21 +382,36 @@ func (repo *IAMRepository) SearchIAMMembersx(ctx context.Context, request *iam_m
return result, nil
}
-func (repo *IAMRepository) GetDefaultMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error) {
- text, err := repo.View.MessageTexts(repo.SystemDefaults.IamID)
- if err != nil {
- return nil, err
+func (repo *IAMRepository) GetDefaultMessageText(ctx context.Context, textType, lang string) (*domain.CustomMessageText, error) {
+ repo.mutex.Lock()
+ defer repo.mutex.Unlock()
+ var err error
+ contents, ok := repo.NotificationTranslationFileContents[lang]
+ if !ok {
+ contents, err = repo.readTranslationFile(repo.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", lang))
+ if caos_errs.IsNotFound(err) {
+ contents, err = repo.readTranslationFile(repo.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", repo.SystemDefaults.DefaultLanguage.String()))
+ }
+ if err != nil {
+ return nil, err
+ }
+ repo.NotificationTranslationFileContents[lang] = contents
}
- return iam_es_model.MessageTextsViewToModel(text, true), err
+ messageTexts := new(domain.MessageTexts)
+ if err := yaml.Unmarshal(contents, messageTexts); err != nil {
+ return nil, caos_errs.ThrowInternal(err, "TEXT-3N9fs", "Errors.TranslationFile.ReadError")
+ }
+ return messageTexts.GetMessageTextByType(textType), nil
}
-func (repo *IAMRepository) GetDefaultMessageText(ctx context.Context, textType, lang string) (*iam_model.MessageTextView, error) {
- text, err := repo.View.MessageTextByIDs(repo.SystemDefaults.IamID, textType, lang)
+func (repo *IAMRepository) GetCustomMessageText(ctx context.Context, textType, lang string) (*domain.CustomMessageText, error) {
+ texts, err := repo.View.CustomTextsByAggregateIDAndTemplateAndLand(repo.SystemDefaults.IamID, textType, lang)
if err != nil {
return nil, err
}
- text.Default = true
- return iam_es_model.MessageTextViewToModel(text), err
+ result := iam_es_model.CustomTextViewsToMessageDomain(repo.SystemDefaults.IamID, lang, texts)
+ result.Default = true
+ return result, err
}
func (repo *IAMRepository) GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) {
@@ -412,17 +444,17 @@ func (repo *IAMRepository) GetDefaultPrivacyPolicy(ctx context.Context) (*iam_mo
func (repo *IAMRepository) GetDefaultLoginTexts(ctx context.Context, lang string) (*domain.CustomLoginText, error) {
repo.mutex.Lock()
defer repo.mutex.Unlock()
- contents, ok := repo.TranslationFileContents[lang]
+ contents, ok := repo.LoginTranslationFileContents[lang]
var err error
if !ok {
- contents, err = repo.readTranslationFile(fmt.Sprintf("/i18n/%s.yaml", lang))
+ contents, err = repo.readTranslationFile(repo.LoginDir, fmt.Sprintf("/i18n/%s.yaml", lang))
if caos_errs.IsNotFound(err) {
- contents, err = repo.readTranslationFile(fmt.Sprintf("/i18n/%s.yaml", repo.SystemDefaults.DefaultLanguage.String()))
+ contents, err = repo.readTranslationFile(repo.LoginDir, fmt.Sprintf("/i18n/%s.yaml", repo.SystemDefaults.DefaultLanguage.String()))
}
if err != nil {
return nil, err
}
- repo.TranslationFileContents[lang] = contents
+ repo.LoginTranslationFileContents[lang] = contents
}
loginText := new(domain.CustomLoginText)
if err := yaml.Unmarshal(contents, loginText); err != nil {
@@ -447,8 +479,8 @@ func (repo *IAMRepository) getIAMEvents(ctx context.Context, sequence uint64) ([
return repo.Eventstore.FilterEvents(ctx, query)
}
-func (repo *IAMRepository) readTranslationFile(filename string) ([]byte, error) {
- r, err := repo.LoginDir.Open(filename)
+func (repo *IAMRepository) readTranslationFile(dir http.FileSystem, filename string) ([]byte, error) {
+ r, err := dir.Open(filename)
if os.IsNotExist(err) {
return nil, caos_errs.ThrowNotFound(err, "TEXT-3n9fs", "Errors.TranslationFile.NotFound")
}
diff --git a/internal/admin/repository/eventsourcing/repository.go b/internal/admin/repository/eventsourcing/repository.go
index 933148cb16..c956507300 100644
--- a/internal/admin/repository/eventsourcing/repository.go
+++ b/internal/admin/repository/eventsourcing/repository.go
@@ -54,6 +54,9 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, s
statikLoginFS, err := fs.NewWithNamespace("login")
logging.Log("CONFI-7usEW").OnError(err).Panic("unable to start login statik dir")
+ statikNotificationFS, err := fs.NewWithNamespace("notification")
+ logging.Log("CONFI-7usEW").OnError(err).Panic("unable to start notification statik dir")
+
return &EsRepository{
spooler: spool,
OrgRepo: eventstore.OrgRepo{
@@ -63,14 +66,16 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, s
SystemDefaults: systemDefaults,
},
IAMRepository: eventstore.IAMRepository{
- Eventstore: es,
- View: view,
- SystemDefaults: systemDefaults,
- SearchLimit: conf.SearchLimit,
- Roles: roles,
- PrefixAvatarURL: assetsAPI,
- LoginDir: statikLoginFS,
- TranslationFileContents: make(map[string][]byte),
+ Eventstore: es,
+ View: view,
+ SystemDefaults: systemDefaults,
+ SearchLimit: conf.SearchLimit,
+ Roles: roles,
+ PrefixAvatarURL: assetsAPI,
+ LoginDir: statikLoginFS,
+ NotificationDir: statikNotificationFS,
+ LoginTranslationFileContents: make(map[string][]byte),
+ NotificationTranslationFileContents: make(map[string][]byte),
},
AdministratorRepo: eventstore.AdministratorRepo{
View: view,
diff --git a/internal/admin/repository/iam.go b/internal/admin/repository/iam.go
index 781a53ae7c..8523cc18c2 100644
--- a/internal/admin/repository/iam.go
+++ b/internal/admin/repository/iam.go
@@ -2,6 +2,9 @@ package repository
import (
"context"
+
+ "golang.org/x/text/language"
+
"github.com/caos/zitadel/internal/domain"
usr_model "github.com/caos/zitadel/internal/user/model"
@@ -9,6 +12,8 @@ import (
)
type IAMRepository interface {
+ Languages(ctx context.Context) ([]language.Tag, error)
+
SearchIAMMembers(ctx context.Context, request *iam_model.IAMMemberSearchRequest) (*iam_model.IAMMemberSearchResponse, error)
GetIAMMemberRoles() []string
@@ -29,8 +34,8 @@ type IAMRepository interface {
GetDefaultMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error)
- GetDefaultMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error)
- GetDefaultMessageText(ctx context.Context, textType string, language string) (*iam_model.MessageTextView, error)
+ GetDefaultMessageText(ctx context.Context, textType, language string) (*domain.CustomMessageText, error)
+ GetCustomMessageText(ctx context.Context, textType string, language string) (*domain.CustomMessageText, error)
GetDefaultLoginTexts(ctx context.Context, lang string) (*domain.CustomLoginText, error)
GetCustomLoginTexts(ctx context.Context, lang string) (*domain.CustomLoginText, error)
diff --git a/internal/api/grpc/admin/custom_text.go b/internal/api/grpc/admin/custom_text.go
index e765ce3931..aa61f4b3e1 100644
--- a/internal/api/grpc/admin/custom_text.go
+++ b/internal/api/grpc/admin/custom_text.go
@@ -15,7 +15,17 @@ func (s *Server) GetDefaultInitMessageText(ctx context.Context, req *admin_pb.Ge
return nil, err
}
return &admin_pb.GetDefaultInitMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
+ }, nil
+}
+
+func (s *Server) GetCustomInitMessageText(ctx context.Context, req *admin_pb.GetCustomInitMessageTextRequest) (*admin_pb.GetCustomInitMessageTextResponse, error) {
+ msg, err := s.iam.GetCustomMessageText(ctx, domain.InitCodeMessageType, req.Language)
+ if err != nil {
+ return nil, err
+ }
+ return &admin_pb.GetCustomInitMessageTextResponse{
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -39,7 +49,17 @@ func (s *Server) GetDefaultPasswordResetMessageText(ctx context.Context, req *ad
return nil, err
}
return &admin_pb.GetDefaultPasswordResetMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
+ }, nil
+}
+
+func (s *Server) GetCustomPasswordResetMessageText(ctx context.Context, req *admin_pb.GetCustomPasswordResetMessageTextRequest) (*admin_pb.GetCustomPasswordResetMessageTextResponse, error) {
+ msg, err := s.iam.GetCustomMessageText(ctx, domain.PasswordResetMessageType, req.Language)
+ if err != nil {
+ return nil, err
+ }
+ return &admin_pb.GetCustomPasswordResetMessageTextResponse{
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -63,7 +83,17 @@ func (s *Server) GetDefaultVerifyEmailMessageText(ctx context.Context, req *admi
return nil, err
}
return &admin_pb.GetDefaultVerifyEmailMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
+ }, nil
+}
+
+func (s *Server) GetCustomVerifyEmailMessageText(ctx context.Context, req *admin_pb.GetCustomVerifyEmailMessageTextRequest) (*admin_pb.GetCustomVerifyEmailMessageTextResponse, error) {
+ msg, err := s.iam.GetCustomMessageText(ctx, domain.VerifyEmailMessageType, req.Language)
+ if err != nil {
+ return nil, err
+ }
+ return &admin_pb.GetCustomVerifyEmailMessageTextResponse{
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -87,7 +117,17 @@ func (s *Server) GetDefaultVerifyPhoneMessageText(ctx context.Context, req *admi
return nil, err
}
return &admin_pb.GetDefaultVerifyPhoneMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
+ }, nil
+}
+
+func (s *Server) GetCustomVerifyPhoneMessageText(ctx context.Context, req *admin_pb.GetCustomVerifyPhoneMessageTextRequest) (*admin_pb.GetCustomVerifyPhoneMessageTextResponse, error) {
+ msg, err := s.iam.GetCustomMessageText(ctx, domain.VerifyPhoneMessageType, req.Language)
+ if err != nil {
+ return nil, err
+ }
+ return &admin_pb.GetCustomVerifyPhoneMessageTextResponse{
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -111,7 +151,17 @@ func (s *Server) GetDefaultDomainClaimedMessageText(ctx context.Context, req *ad
return nil, err
}
return &admin_pb.GetDefaultDomainClaimedMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
+ }, nil
+}
+
+func (s *Server) GetCustomDomainClaimedMessageText(ctx context.Context, req *admin_pb.GetCustomDomainClaimedMessageTextRequest) (*admin_pb.GetCustomDomainClaimedMessageTextResponse, error) {
+ msg, err := s.iam.GetCustomMessageText(ctx, domain.DomainClaimedMessageType, req.Language)
+ if err != nil {
+ return nil, err
+ }
+ return &admin_pb.GetCustomDomainClaimedMessageTextResponse{
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
diff --git a/internal/api/grpc/admin/language.go b/internal/api/grpc/admin/language.go
new file mode 100644
index 0000000000..9257bed7ef
--- /dev/null
+++ b/internal/api/grpc/admin/language.go
@@ -0,0 +1,16 @@
+package admin
+
+import (
+ "context"
+
+ "github.com/caos/zitadel/internal/api/grpc/text"
+ admin_pb "github.com/caos/zitadel/pkg/grpc/admin"
+)
+
+func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) {
+ langs, err := s.iam.Languages(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &admin_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
+}
diff --git a/internal/api/grpc/auth/language.go b/internal/api/grpc/auth/language.go
new file mode 100644
index 0000000000..f794d4c9ea
--- /dev/null
+++ b/internal/api/grpc/auth/language.go
@@ -0,0 +1,16 @@
+package auth
+
+import (
+ "context"
+
+ "github.com/caos/zitadel/internal/api/grpc/text"
+ auth_pb "github.com/caos/zitadel/pkg/grpc/auth"
+)
+
+func (s *Server) GetSupportedLanguages(ctx context.Context, req *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) {
+ langs, err := s.repo.Languages(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &auth_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
+}
diff --git a/internal/api/grpc/management/custom_text.go b/internal/api/grpc/management/custom_text.go
index 0d2c4d8dac..805292149f 100644
--- a/internal/api/grpc/management/custom_text.go
+++ b/internal/api/grpc/management/custom_text.go
@@ -18,7 +18,7 @@ func (s *Server) GetCustomInitMessageText(ctx context.Context, req *mgmt_pb.GetC
return nil, err
}
return &mgmt_pb.GetCustomInitMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -28,7 +28,7 @@ func (s *Server) GetDefaultInitMessageText(ctx context.Context, req *mgmt_pb.Get
return nil, err
}
return &mgmt_pb.GetDefaultInitMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -66,7 +66,7 @@ func (s *Server) GetCustomPasswordResetMessageText(ctx context.Context, req *mgm
return nil, err
}
return &mgmt_pb.GetCustomPasswordResetMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -76,7 +76,7 @@ func (s *Server) GetDefaultPasswordResetMessageText(ctx context.Context, req *mg
return nil, err
}
return &mgmt_pb.GetDefaultPasswordResetMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -114,7 +114,7 @@ func (s *Server) GetCustomVerifyEmailMessageText(ctx context.Context, req *mgmt_
return nil, err
}
return &mgmt_pb.GetCustomVerifyEmailMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -124,7 +124,7 @@ func (s *Server) GetDefaultVerifyEmailMessageText(ctx context.Context, req *mgmt
return nil, err
}
return &mgmt_pb.GetDefaultVerifyEmailMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -162,7 +162,7 @@ func (s *Server) GetCustomVerifyPhoneMessageText(ctx context.Context, req *mgmt_
return nil, err
}
return &mgmt_pb.GetCustomVerifyPhoneMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -172,7 +172,7 @@ func (s *Server) GetDefaultVerifyPhoneMessageText(ctx context.Context, req *mgmt
return nil, err
}
return &mgmt_pb.GetDefaultVerifyPhoneMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -210,7 +210,7 @@ func (s *Server) GetCustomDomainClaimedMessageText(ctx context.Context, req *mgm
return nil, err
}
return &mgmt_pb.GetCustomDomainClaimedMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
@@ -220,7 +220,7 @@ func (s *Server) GetDefaultDomainClaimedMessageText(ctx context.Context, req *mg
return nil, err
}
return &mgmt_pb.GetDefaultDomainClaimedMessageTextResponse{
- CustomText: text_grpc.ModelCustomMsgTextToPb(msg),
+ CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
diff --git a/internal/api/grpc/management/language.go b/internal/api/grpc/management/language.go
new file mode 100644
index 0000000000..68327bfd14
--- /dev/null
+++ b/internal/api/grpc/management/language.go
@@ -0,0 +1,16 @@
+package management
+
+import (
+ "context"
+
+ "github.com/caos/zitadel/internal/api/grpc/text"
+ mgmt_pb "github.com/caos/zitadel/pkg/grpc/management"
+)
+
+func (s *Server) GetSupportedLanguages(ctx context.Context, req *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) {
+ langs, err := s.org.Languages(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil
+}
diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go
index 600702e5d7..e2ffb2c5f8 100644
--- a/internal/api/grpc/management/user_converter.go
+++ b/internal/api/grpc/management/user_converter.go
@@ -43,7 +43,7 @@ func AddHumanUserRequestToDomain(req *mgmt_pb.AddHumanUserRequest) *domain.Human
Username: req.UserName,
}
preferredLanguage, err := language.Parse(req.Profile.PreferredLanguage)
- logging.Log("MANAG-3GUFJ").OnError(err).Debug("language malformed")
+ logging.Log("MANAG-M029f").OnError(err).Debug("language malformed")
h.Profile = &domain.Profile{
FirstName: req.Profile.FirstName,
LastName: req.Profile.LastName,
diff --git a/internal/api/grpc/text/custom_text.go b/internal/api/grpc/text/custom_text.go
index 9763d9d740..3e25c98ac1 100644
--- a/internal/api/grpc/text/custom_text.go
+++ b/internal/api/grpc/text/custom_text.go
@@ -3,11 +3,10 @@ package text
import (
"github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/domain"
- "github.com/caos/zitadel/internal/iam/model"
text_pb "github.com/caos/zitadel/pkg/grpc/text"
)
-func ModelCustomMsgTextToPb(msg *model.MessageTextView) *text_pb.MessageCustomText {
+func DomainCustomMsgTextToPb(msg *domain.CustomMessageText) *text_pb.MessageCustomText {
return &text_pb.MessageCustomText{
Title: msg.Title,
PreHeader: msg.PreHeader,
diff --git a/internal/api/grpc/text/language.go b/internal/api/grpc/text/language.go
new file mode 100644
index 0000000000..9ae5b1ed1a
--- /dev/null
+++ b/internal/api/grpc/text/language.go
@@ -0,0 +1,13 @@
+package text
+
+import (
+ "golang.org/x/text/language"
+)
+
+func LanguageTagsToStrings(langs []language.Tag) []string {
+ result := make([]string, len(langs))
+ for i, lang := range langs {
+ result[i] = lang.String()
+ }
+ return result
+}
diff --git a/internal/auth/repository/eventsourcing/eventstore/iam.go b/internal/auth/repository/eventsourcing/eventstore/iam.go
index 3fd493e4ae..b5d37c705b 100644
--- a/internal/auth/repository/eventsourcing/eventstore/iam.go
+++ b/internal/auth/repository/eventsourcing/eventstore/iam.go
@@ -2,15 +2,35 @@ package eventstore
import (
"context"
+ "net/http"
+
+ "github.com/caos/logging"
+ "golang.org/x/text/language"
+
+ "github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/internal/iam/model"
)
type IAMRepository struct {
- IAMID string
+ IAMID string
+ LoginDir http.FileSystem
IAMV2QuerySide *query.Queries
+ supportedLangs []language.Tag
+}
+
+func (repo *IAMRepository) Languages(ctx context.Context) ([]language.Tag, error) {
+ if len(repo.supportedLangs) == 0 {
+ langs, err := i18n.SupportedLanguages(repo.LoginDir)
+ if err != nil {
+ logging.Log("ADMIN-tiMWs").WithError(err).Debug("unable to parse language")
+ return nil, err
+ }
+ repo.supportedLangs = langs
+ }
+ return repo.supportedLangs, nil
}
func (repo *IAMRepository) GetIAM(ctx context.Context) (*model.IAM, error) {
diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go
index 0373c2fedf..4c75bc7146 100644
--- a/internal/auth/repository/eventsourcing/repository.go
+++ b/internal/auth/repository/eventsourcing/repository.go
@@ -3,6 +3,9 @@ package eventsourcing
import (
"context"
+ "github.com/caos/logging"
+ "github.com/rakyll/statik/fs"
+
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/eventstore"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/spooler"
@@ -76,6 +79,9 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co
return nil, err
}
+ statikLoginFS, err := fs.NewWithNamespace("login")
+ logging.Log("CONFI-20opp").OnError(err).Panic("unable to start login statik dir")
+
keyChan := make(chan *key_model.KeyView)
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, systemDefaults, keyChan)
locker := spooler.NewLocker(sqlClient)
@@ -153,6 +159,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co
},
eventstore.IAMRepository{
IAMID: systemDefaults.IamID,
+ LoginDir: statikLoginFS,
IAMV2QuerySide: queries,
},
eventstore.FeaturesRepo{
diff --git a/internal/auth/repository/iam.go b/internal/auth/repository/iam.go
index 6d7e9efd67..fb0ba1885f 100644
--- a/internal/auth/repository/iam.go
+++ b/internal/auth/repository/iam.go
@@ -3,9 +3,12 @@ package repository
import (
"context"
+ "golang.org/x/text/language"
+
"github.com/caos/zitadel/internal/iam/model"
)
type IAMRepository interface {
+ Languages(ctx context.Context) ([]language.Tag, error)
GetIAM(ctx context.Context) (*model.IAM, error)
}
diff --git a/internal/domain/custom_messge_text.go b/internal/domain/custom_message_text.go
similarity index 64%
rename from internal/domain/custom_messge_text.go
rename to internal/domain/custom_message_text.go
index 136bfd836f..ed457d2aaf 100644
--- a/internal/domain/custom_messge_text.go
+++ b/internal/domain/custom_message_text.go
@@ -21,6 +21,14 @@ const (
MessageFooterText = "Footer"
)
+type MessageTexts struct {
+ InitCode CustomMessageText
+ PasswordReset CustomMessageText
+ VerifyEmail CustomMessageText
+ VerifyPhone CustomMessageText
+ DomainClaimed CustomMessageText
+}
+
type CustomMessageText struct {
models.ObjectRoot
@@ -40,3 +48,19 @@ type CustomMessageText struct {
func (m *CustomMessageText) IsValid() bool {
return m.MessageTextType != "" && m.Language != language.Und
}
+
+func (m *MessageTexts) GetMessageTextByType(msgType string) *CustomMessageText {
+ switch msgType {
+ case InitCodeMessageType:
+ return &m.InitCode
+ case PasswordResetMessageType:
+ return &m.PasswordReset
+ case VerifyEmailMessageType:
+ return &m.VerifyEmail
+ case VerifyPhoneMessageType:
+ return &m.VerifyPhone
+ case DomainClaimedMessageType:
+ return &m.DomainClaimed
+ }
+ return nil
+}
diff --git a/internal/iam/model/message_text_view.go b/internal/iam/model/message_text_view.go
index 86b42cc7a7..1348d0d96e 100644
--- a/internal/iam/model/message_text_view.go
+++ b/internal/iam/model/message_text_view.go
@@ -8,10 +8,6 @@ import (
"github.com/caos/zitadel/internal/domain"
)
-type MessageTextsView struct {
- Texts []*MessageTextView
- Default bool
-}
type MessageTextView struct {
AggregateID string
MessageTextType string
diff --git a/internal/iam/repository/view/model/custom_text.go b/internal/iam/repository/view/model/custom_text.go
index b518c82bda..5c5f72b6fe 100644
--- a/internal/iam/repository/view/model/custom_text.go
+++ b/internal/iam/repository/view/model/custom_text.go
@@ -110,6 +110,46 @@ func (r *CustomTextView) IsMessageTemplate() bool {
r.Template == domain.DomainClaimedMessageType
}
+func CustomTextViewsToMessageDomain(aggregateID, lang string, texts []*CustomTextView) *domain.CustomMessageText {
+ langTag := language.Make(lang)
+ result := &domain.CustomMessageText{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: aggregateID,
+ },
+ Language: langTag,
+ }
+ for _, text := range texts {
+ if text.CreationDate.Before(result.CreationDate) {
+ result.CreationDate = text.CreationDate
+ }
+ if text.ChangeDate.After(result.ChangeDate) {
+ result.ChangeDate = text.ChangeDate
+ }
+ if text.Key == domain.MessageTitle {
+ result.Title = text.Text
+ }
+ if text.Key == domain.MessagePreHeader {
+ result.PreHeader = text.Text
+ }
+ if text.Key == domain.MessageSubject {
+ result.Subject = text.Text
+ }
+ if text.Key == domain.MessageGreeting {
+ result.Greeting = text.Text
+ }
+ if text.Key == domain.MessageText {
+ result.Text = text.Text
+ }
+ if text.Key == domain.MessageButtonText {
+ result.ButtonText = text.Text
+ }
+ if text.Key == domain.MessageFooterText {
+ result.FooterText = text.Text
+ }
+ }
+ return result
+}
+
func CustomTextViewsToLoginDomain(aggregateID, lang string, texts []*CustomTextView) *domain.CustomLoginText {
langTag := language.Make(lang)
result := &domain.CustomLoginText{
diff --git a/internal/iam/repository/view/model/message_text.go b/internal/iam/repository/view/model/message_text.go
index b5ec8c90dc..3e096bda7e 100644
--- a/internal/iam/repository/view/model/message_text.go
+++ b/internal/iam/repository/view/model/message_text.go
@@ -59,12 +59,6 @@ func MessageTextViewFromModel(template *model.MessageTextView) *MessageTextView
}
}
-func MessageTextsViewToModel(textsIn []*MessageTextView, defaultIn bool) *model.MessageTextsView {
- return &model.MessageTextsView{
- Texts: messageTextsViewToModelArr(textsIn, defaultIn),
- }
-}
-
func messageTextsViewToModelArr(texts []*MessageTextView, defaultIn bool) []*model.MessageTextView {
result := make([]*model.MessageTextView, len(texts))
for i, r := range texts {
diff --git a/internal/management/repository/eventsourcing/eventstore/org.go b/internal/management/repository/eventsourcing/eventstore/org.go
index 099977c3c6..d4e5b86010 100644
--- a/internal/management/repository/eventsourcing/eventstore/org.go
+++ b/internal/management/repository/eventsourcing/eventstore/org.go
@@ -14,6 +14,7 @@ import (
"github.com/caos/logging"
"github.com/ghodss/yaml"
"github.com/golang/protobuf/ptypes"
+ "golang.org/x/text/language"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/config/systemdefaults"
@@ -21,6 +22,7 @@ import (
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/eventstore/v1/models"
+ "github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_view "github.com/caos/zitadel/internal/iam/repository/view"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model"
@@ -37,15 +39,30 @@ import (
)
type OrgRepository struct {
- 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
+ SearchLimit uint64
+ Eventstore v1.Eventstore
+ View *mgmt_view.View
+ Roles []string
+ SystemDefaults systemdefaults.SystemDefaults
+ PrefixAvatarURL string
+ LoginDir http.FileSystem
+ NotificationDir http.FileSystem
+ LoginTranslationFileContents map[string][]byte
+ NotificationTranslationFileContents map[string][]byte
+ mutex sync.Mutex
+ supportedLangs []language.Tag
+}
+
+func (repo *OrgRepository) Languages(ctx context.Context) ([]language.Tag, error) {
+ if len(repo.supportedLangs) == 0 {
+ langs, err := i18n.SupportedLanguages(repo.LoginDir)
+ if err != nil {
+ logging.Log("ADMIN-tiMWs").WithError(err).Debug("unable to parse language")
+ return nil, err
+ }
+ repo.supportedLangs = langs
+ }
+ return repo.supportedLangs, nil
}
func (repo *OrgRepository) OrgByID(ctx context.Context, id string) (*org_model.OrgView, error) {
@@ -576,68 +593,74 @@ func (repo *OrgRepository) GetMailTemplate(ctx context.Context) (*iam_model.Mail
return iam_es_model.MailTemplateViewToModel(template), err
}
-func (repo *OrgRepository) GetDefaultMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error) {
- texts, err := repo.View.MessageTextsByAggregateID(repo.SystemDefaults.IamID)
- if err != nil {
- return nil, err
- }
- return iam_es_model.MessageTextsViewToModel(texts, true), err
-}
-
-func (repo *OrgRepository) GetMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error) {
- defaultIn := false
- texts, err := repo.View.MessageTextsByAggregateID(authz.GetCtxData(ctx).OrgID)
- if errors.IsNotFound(err) || len(texts) == 0 {
- texts, err = repo.View.MessageTextsByAggregateID(repo.SystemDefaults.IamID)
+func (repo *OrgRepository) GetDefaultMessageText(ctx context.Context, textType, lang string) (*domain.CustomMessageText, error) {
+ repo.mutex.Lock()
+ defer repo.mutex.Unlock()
+ var err error
+ contents, ok := repo.NotificationTranslationFileContents[lang]
+ if !ok {
+ contents, err = repo.readTranslationFile(repo.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", lang))
+ if errors.IsNotFound(err) {
+ contents, err = repo.readTranslationFile(repo.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", repo.SystemDefaults.DefaultLanguage.String()))
+ }
if err != nil {
return nil, err
}
- defaultIn = true
+ repo.NotificationTranslationFileContents[lang] = contents
}
+ notificationTextMap := make(map[string]interface{})
+ if err := yaml.Unmarshal(contents, ¬ificationTextMap); err != nil {
+ return nil, errors.ThrowInternal(err, "TEXT-093sd", "Errors.TranslationFile.ReadError")
+ }
+ texts, err := repo.View.CustomTextsByAggregateIDAndTemplateAndLand(repo.SystemDefaults.IamID, textType, lang)
if err != nil {
return nil, err
}
- return iam_es_model.MessageTextsViewToModel(texts, defaultIn), err
-}
-
-func (repo *OrgRepository) GetDefaultMessageText(ctx context.Context, textType, lang string) (*iam_model.MessageTextView, error) {
- text, err := repo.View.MessageTextByIDs(repo.SystemDefaults.IamID, textType, lang)
- if err != nil {
- return nil, err
- }
- text.Default = true
- return iam_es_model.MessageTextViewToModel(text), err
-}
-
-func (repo *OrgRepository) GetMessageText(ctx context.Context, orgID, textType, lang string) (*iam_model.MessageTextView, error) {
- text, err := repo.View.MessageTextByIDs(orgID, textType, lang)
- if errors.IsNotFound(err) {
- result, err := repo.GetDefaultMessageText(ctx, textType, lang)
- if err != nil {
- return nil, err
+ for _, text := range texts {
+ messageTextMap, ok := notificationTextMap[textType].(map[string]interface{})
+ if !ok {
+ continue
}
- return result, nil
+ messageTextMap[text.Key] = text.Text
}
+ jsonbody, err := json.Marshal(notificationTextMap)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "TEXT-02m8f", "Errors.TranslationFile.MergeError")
+ }
+ notificationText := new(domain.MessageTexts)
+ if err := json.Unmarshal(jsonbody, ¬ificationText); err != nil {
+ return nil, errors.ThrowInternal(err, "TEXT-20ops", "Errors.TranslationFile.MergeError")
+ }
+ result := notificationText.GetMessageTextByType(textType)
+ result.Default = true
+ return result, nil
+}
+
+func (repo *OrgRepository) GetMessageText(ctx context.Context, orgID, textType, lang string) (*domain.CustomMessageText, error) {
+ texts, err := repo.View.CustomTextsByAggregateIDAndTemplateAndLand(orgID, textType, lang)
if err != nil {
return nil, err
}
- return iam_es_model.MessageTextViewToModel(text), err
+ if len(texts) == 0 {
+ return repo.GetDefaultMessageText(ctx, textType, lang)
+ }
+ return iam_es_model.CustomTextViewsToMessageDomain(repo.SystemDefaults.IamID, lang, texts), 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]
+ contents, ok := repo.LoginTranslationFileContents[lang]
var err error
if !ok {
- contents, err = repo.readTranslationFile(fmt.Sprintf("/i18n/%s.yaml", lang))
+ contents, err = repo.readTranslationFile(repo.LoginDir, fmt.Sprintf("/i18n/%s.yaml", lang))
if errors.IsNotFound(err) {
- contents, err = repo.readTranslationFile(fmt.Sprintf("/i18n/%s.yaml", repo.SystemDefaults.DefaultLanguage.String()))
+ contents, err = repo.readTranslationFile(repo.LoginDir, fmt.Sprintf("/i18n/%s.yaml", repo.SystemDefaults.DefaultLanguage.String()))
}
if err != nil {
return nil, err
}
- repo.TranslationFileContents[lang] = contents
+ repo.LoginTranslationFileContents[lang] = contents
}
loginTextMap := make(map[string]interface{})
if err := yaml.Unmarshal(contents, &loginTextMap); err != nil {
@@ -770,8 +793,8 @@ 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)
+func (repo *OrgRepository) readTranslationFile(dir http.FileSystem, filename string) ([]byte, error) {
+ r, err := dir.Open(filename)
if os.IsNotExist(err) {
return nil, errors.ThrowNotFound(err, "TEXT-93nfl", "Errors.TranslationFile.NotFound")
}
diff --git a/internal/management/repository/eventsourcing/repository.go b/internal/management/repository/eventsourcing/repository.go
index cf7b575c99..1a0c38989b 100644
--- a/internal/management/repository/eventsourcing/repository.go
+++ b/internal/management/repository/eventsourcing/repository.go
@@ -58,17 +58,22 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, roles []string, querie
statikLoginFS, err := fs.NewWithNamespace("login")
logging.Log("CONFI-7usEW").OnError(err).Panic("unable to start login statik dir")
+ statikNotificationFS, err := fs.NewWithNamespace("notification")
+ logging.Log("CONFI-7usEW").OnError(err).Panic("unable to start notification statik dir")
+
return &EsRepository{
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),
+ SearchLimit: conf.SearchLimit,
+ Eventstore: es,
+ View: view,
+ Roles: roles,
+ SystemDefaults: systemDefaults,
+ PrefixAvatarURL: assetsAPI,
+ LoginDir: statikLoginFS,
+ NotificationDir: statikNotificationFS,
+ LoginTranslationFileContents: make(map[string][]byte),
+ NotificationTranslationFileContents: make(map[string][]byte),
},
ProjectRepo: eventstore.ProjectRepo{es, conf.SearchLimit, view, roles, systemDefaults.IamID, assetsAPI},
UserRepo: eventstore.UserRepo{es, conf.SearchLimit, view, systemDefaults, assetsAPI},
diff --git a/internal/management/repository/org.go b/internal/management/repository/org.go
index de8594aa77..f0c683ae41 100644
--- a/internal/management/repository/org.go
+++ b/internal/management/repository/org.go
@@ -4,6 +4,8 @@ import (
"context"
"time"
+ "golang.org/x/text/language"
+
"github.com/caos/zitadel/internal/domain"
iam_model "github.com/caos/zitadel/internal/iam/model"
@@ -11,6 +13,7 @@ import (
)
type OrgRepository interface {
+ Languages(ctx context.Context) ([]language.Tag, error)
OrgByID(ctx context.Context, id string) (*org_model.OrgView, error)
OrgByDomainGlobal(ctx context.Context, domain string) (*org_model.OrgView, error)
OrgChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, auditLogRetention time.Duration) (*org_model.OrgChanges, error)
@@ -48,10 +51,8 @@ type OrgRepository interface {
GetDefaultMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error)
GetMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error)
- GetDefaultMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error)
- GetMessageTexts(ctx context.Context) (*iam_model.MessageTextsView, error)
- GetDefaultMessageText(ctx context.Context, textType string, language string) (*iam_model.MessageTextView, error)
- GetMessageText(ctx context.Context, orgID, textType, language string) (*iam_model.MessageTextView, error)
+ GetDefaultMessageText(ctx context.Context, textType string, language string) (*domain.CustomMessageText, error)
+ GetMessageText(ctx context.Context, orgID, textType, lang string) (*domain.CustomMessageText, error)
GetDefaultLoginTexts(ctx context.Context, lang string) (*domain.CustomLoginText, error)
GetLoginTexts(ctx context.Context, orgID, lang string) (*domain.CustomLoginText, error)
diff --git a/internal/notification/repository/eventsourcing/handler/custom_text.go b/internal/notification/repository/eventsourcing/handler/custom_text.go
new file mode 100644
index 0000000000..90d05fe1ee
--- /dev/null
+++ b/internal/notification/repository/eventsourcing/handler/custom_text.go
@@ -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)
+}
diff --git a/internal/notification/repository/eventsourcing/handler/handler.go b/internal/notification/repository/eventsourcing/handler/handler.go
index 310993bfed..5683b72f86 100644
--- a/internal/notification/repository/eventsourcing/handler/handler.go
+++ b/internal/notification/repository/eventsourcing/handler/handler.go
@@ -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,
),
diff --git a/internal/notification/repository/eventsourcing/handler/notification.go b/internal/notification/repository/eventsourcing/handler/notification.go
index aafd1bc3a1..3e3520173a 100644
--- a/internal/notification/repository/eventsourcing/handler/notification.go
+++ b/internal/notification/repository/eventsourcing/handler/notification.go
@@ -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)
diff --git a/internal/notification/repository/eventsourcing/repository.go b/internal/notification/repository/eventsourcing/repository.go
index 22c5689b25..3c9e02d057 100644
--- a/internal/notification/repository/eventsourcing/repository.go
+++ b/internal/notification/repository/eventsourcing/repository.go
@@ -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,
diff --git a/internal/notification/repository/eventsourcing/spooler/spooler.go b/internal/notification/repository/eventsourcing/spooler/spooler.go
index 0dcf6f09aa..1ac2abf53c 100644
--- a/internal/notification/repository/eventsourcing/spooler/spooler.go
+++ b/internal/notification/repository/eventsourcing/spooler/spooler.go
@@ -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()
diff --git a/internal/notification/repository/eventsourcing/view/custom_texts.go b/internal/notification/repository/eventsourcing/view/custom_texts.go
new file mode 100644
index 0000000000..967e69e539
--- /dev/null
+++ b/internal/notification/repository/eventsourcing/view/custom_texts.go
@@ -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)
+}
diff --git a/internal/notification/templates/templateData.go b/internal/notification/templates/templateData.go
index 3197b52c3b..9e2b103ae8 100644
--- a/internal/notification/templates/templateData.go
+++ b/internal/notification/templates/templateData.go
@@ -5,6 +5,7 @@ import (
"html"
"strings"
+ "github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
)
@@ -16,10 +17,6 @@ const (
defaultFontColor = "#22292f"
defaultBackgroundColor = "#fafafa"
defaultPrimaryColor = "#5282C1"
- //defaultOrgName = "CAOS AG"
- //defaultOrgURL = "http://www.caos.ch"
- //defaultFooter1 = "Teufener Strasse 19"
- //defaultFooter2 = "CH-9000 St. Gallen"
)
type TemplateData struct {
@@ -41,28 +38,19 @@ type TemplateData struct {
FooterText string
}
-func (data *TemplateData) Translate(i18n *i18n.Translator, args map[string]interface{}, langs ...string) {
- data.Title = i18n.Localize(data.Title, nil, langs...)
- data.PreHeader = i18n.Localize(data.PreHeader, nil, langs...)
- data.Subject = i18n.Localize(data.Subject, nil, langs...)
- data.Greeting = i18n.Localize(data.Greeting, args, langs...)
- data.Text = html.UnescapeString(i18n.Localize(data.Text, args, langs...))
- if data.Href != "" {
- data.Href = i18n.Localize(data.Href, nil, langs...)
- }
- data.ButtonText = i18n.Localize(data.ButtonText, nil, langs...)
+func (data *TemplateData) Translate(translator *i18n.Translator, msgType string, args map[string]interface{}, langs ...string) {
+ data.Title = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageTitle), args, langs...)
+ data.PreHeader = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessagePreHeader), args, langs...)
+ data.Subject = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageSubject), args, langs...)
+ data.Greeting = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageGreeting), args, langs...)
+ data.Text = html.UnescapeString(translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageText), args, langs...))
+ data.ButtonText = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageButtonText), args, langs...)
+ data.FooterText = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageFooterText), args, langs...)
}
-func GetTemplateData(apiDomain, href string, text *iam_model.MessageTextView, policy *iam_model.LabelPolicyView) TemplateData {
+func GetTemplateData(translator *i18n.Translator, translateArgs map[string]interface{}, apiDomain, href, msgType, lang string, policy *iam_model.LabelPolicyView) TemplateData {
templateData := TemplateData{
- Title: text.Title,
- PreHeader: text.PreHeader,
- Subject: text.Subject,
- Greeting: text.Greeting,
- Text: html.UnescapeString(text.Text),
Href: href,
- ButtonText: text.ButtonText,
- FooterText: text.FooterText,
PrimaryColor: defaultPrimaryColor,
BackgroundColor: defaultBackgroundColor,
FontColor: defaultFontColor,
@@ -71,6 +59,7 @@ func GetTemplateData(apiDomain, href string, text *iam_model.MessageTextView, po
FontFamily: defaultFontFamily,
IncludeFooter: false,
}
+ templateData.Translate(translator, msgType, translateArgs, lang)
if policy.PrimaryColor != "" {
templateData.PrimaryColor = policy.PrimaryColor
}
diff --git a/internal/notification/types/domain_claimed.go b/internal/notification/types/domain_claimed.go
index 58461125a6..7e538c1af6 100644
--- a/internal/notification/types/domain_claimed.go
+++ b/internal/notification/types/domain_claimed.go
@@ -1,10 +1,11 @@
package types
import (
- "html"
"strings"
"github.com/caos/zitadel/internal/config/systemdefaults"
+ "github.com/caos/zitadel/internal/domain"
+ "github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/notification/templates"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
@@ -15,7 +16,7 @@ type DomainClaimedData struct {
URL string
}
-func SendDomainClaimed(mailhtml string, text *iam_model.MessageTextView, user *view_model.NotifyUser, username string, systemDefaults systemdefaults.SystemDefaults, colors *iam_model.LabelPolicyView, apiDomain string) error {
+func SendDomainClaimed(mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, username string, systemDefaults systemdefaults.SystemDefaults, colors *iam_model.LabelPolicyView, apiDomain string) error {
url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.DomainClaimed, &UrlData{UserID: user.ID})
if err != nil {
return err
@@ -24,17 +25,13 @@ func SendDomainClaimed(mailhtml string, text *iam_model.MessageTextView, user *v
args["TempUsername"] = username
args["Domain"] = strings.Split(user.LastEmail, "@")[1]
- text.Greeting, err = templates.ParseTemplateText(text.Greeting, args)
- text.Text, err = templates.ParseTemplateText(text.Text, args)
- text.Text = html.UnescapeString(text.Text)
-
- emailCodeData := &DomainClaimedData{
- TemplateData: templates.GetTemplateData(apiDomain, url, text, colors),
+ domainClaimedData := &DomainClaimedData{
+ TemplateData: templates.GetTemplateData(translator, args, apiDomain, url, domain.DomainClaimedMessageType, user.PreferredLanguage, colors),
URL: url,
}
- template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
+ template, err := templates.GetParsedTemplate(mailhtml, domainClaimedData)
if err != nil {
return err
}
- return generateEmail(user, text.Subject, template, systemDefaults.Notifications, true)
+ return generateEmail(user, domainClaimedData.Subject, template, systemDefaults.Notifications, true)
}
diff --git a/internal/notification/types/email_verification_code.go b/internal/notification/types/email_verification_code.go
index 4b88f17a91..bcc7b7c801 100644
--- a/internal/notification/types/email_verification_code.go
+++ b/internal/notification/types/email_verification_code.go
@@ -1,10 +1,10 @@
package types
import (
- "html"
-
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
+ "github.com/caos/zitadel/internal/domain"
+ "github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
@@ -16,7 +16,7 @@ type EmailVerificationCodeData struct {
URL string
}
-func SendEmailVerificationCode(mailhtml string, text *iam_model.MessageTextView, user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error {
+func SendEmailVerificationCode(mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
@@ -29,12 +29,8 @@ func SendEmailVerificationCode(mailhtml string, text *iam_model.MessageTextView,
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
- text.Greeting, err = templates.ParseTemplateText(text.Greeting, args)
- text.Text, err = templates.ParseTemplateText(text.Text, args)
- text.Text = html.UnescapeString(text.Text)
-
emailCodeData := &EmailVerificationCodeData{
- TemplateData: templates.GetTemplateData(apiDomain, url, text, colors),
+ TemplateData: templates.GetTemplateData(translator, args, apiDomain, url, domain.VerifyEmailMessageType, user.PreferredLanguage, colors),
URL: url,
}
@@ -42,5 +38,5 @@ func SendEmailVerificationCode(mailhtml string, text *iam_model.MessageTextView,
if err != nil {
return err
}
- return generateEmail(user, text.Subject, template, systemDefaults.Notifications, true)
+ return generateEmail(user, emailCodeData.Subject, template, systemDefaults.Notifications, true)
}
diff --git a/internal/notification/types/init_code.go b/internal/notification/types/init_code.go
index 9fce037afe..48546b62ee 100644
--- a/internal/notification/types/init_code.go
+++ b/internal/notification/types/init_code.go
@@ -1,10 +1,10 @@
package types
import (
- "html"
-
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
+ "github.com/caos/zitadel/internal/domain"
+ "github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
@@ -22,7 +22,7 @@ type UrlData struct {
PasswordSet bool
}
-func SendUserInitCode(mailhtml string, text *iam_model.MessageTextView, user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error {
+func SendUserInitCode(mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
@@ -34,17 +34,13 @@ func SendUserInitCode(mailhtml string, text *iam_model.MessageTextView, user *vi
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
- text.Greeting, err = templates.ParseTemplateText(text.Greeting, args)
- text.Text, err = templates.ParseTemplateText(text.Text, args)
- text.Text = html.UnescapeString(text.Text)
-
- emailCodeData := &InitCodeEmailData{
- TemplateData: templates.GetTemplateData(apiDomain, url, text, colors),
+ initCodeData := &InitCodeEmailData{
+ TemplateData: templates.GetTemplateData(translator, args, apiDomain, url, domain.InitCodeMessageType, user.PreferredLanguage, colors),
URL: url,
}
- template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
+ template, err := templates.GetParsedTemplate(mailhtml, initCodeData)
if err != nil {
return err
}
- return generateEmail(user, text.Subject, template, systemDefaults.Notifications, true)
+ return generateEmail(user, initCodeData.Subject, template, systemDefaults.Notifications, true)
}
diff --git a/internal/notification/types/password_code.go b/internal/notification/types/password_code.go
index 1eb1179362..0e8e4d3a00 100644
--- a/internal/notification/types/password_code.go
+++ b/internal/notification/types/password_code.go
@@ -1,10 +1,10 @@
package types
import (
- "html"
-
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
+ "github.com/caos/zitadel/internal/domain"
+ "github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
@@ -18,7 +18,7 @@ type PasswordCodeData struct {
URL string
}
-func SendPasswordCode(mailhtml string, text *iam_model.MessageTextView, user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error {
+func SendPasswordCode(mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
@@ -30,19 +30,15 @@ func SendPasswordCode(mailhtml string, text *iam_model.MessageTextView, user *vi
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
- text.Greeting, err = templates.ParseTemplateText(text.Greeting, args)
- text.Text, err = templates.ParseTemplateText(text.Text, args)
- text.Text = html.UnescapeString(text.Text)
-
- emailCodeData := &PasswordCodeData{
- TemplateData: templates.GetTemplateData(apiDomain, url, text, colors),
+ passwordResetData := &PasswordCodeData{
+ TemplateData: templates.GetTemplateData(translator, args, apiDomain, url, domain.PasswordResetMessageType, user.PreferredLanguage, colors),
FirstName: user.FirstName,
LastName: user.LastName,
URL: url,
}
- template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
+ template, err := templates.GetParsedTemplate(mailhtml, passwordResetData)
if err != nil {
return err
}
- return generateEmail(user, text.Subject, template, systemDefaults.Notifications, true)
+ return generateEmail(user, passwordResetData.Subject, template, systemDefaults.Notifications, true)
}
diff --git a/internal/notification/types/phone_verification_code.go b/internal/notification/types/phone_verification_code.go
index 28447ce2e4..55fe9ad3c2 100644
--- a/internal/notification/types/phone_verification_code.go
+++ b/internal/notification/types/phone_verification_code.go
@@ -1,9 +1,12 @@
package types
import (
+ "fmt"
+
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
- iam_model "github.com/caos/zitadel/internal/iam/model"
+ "github.com/caos/zitadel/internal/domain"
+ "github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
@@ -13,7 +16,7 @@ type PhoneVerificationCodeData struct {
UserID string
}
-func SendPhoneVerificationCode(text *iam_model.MessageTextView, user *view_model.NotifyUser, code *es_model.PhoneCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
+func SendPhoneVerificationCode(translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.PhoneCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
@@ -21,10 +24,10 @@ func SendPhoneVerificationCode(text *iam_model.MessageTextView, user *view_model
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
- text.Text, err = templates.ParseTemplateText(text.Text, args)
+ text := translator.Localize(fmt.Sprintf("%s.%s", domain.VerifyPhoneMessageType, domain.MessageTitle), args, user.PreferredLanguage)
codeData := &PhoneVerificationCodeData{UserID: user.ID}
- template, err := templates.ParseTemplateText(text.Text, codeData)
+ template, err := templates.ParseTemplateText(text, codeData)
if err != nil {
return err
}
diff --git a/migrations/cockroach/V1.55__custom_text.sql b/migrations/cockroach/V1.55__custom_text.sql
new file mode 100644
index 0000000000..99b078f972
--- /dev/null
+++ b/migrations/cockroach/V1.55__custom_text.sql
@@ -0,0 +1,14 @@
+CREATE TABLE notification.custom_texts (
+ aggregate_id TEXT,
+
+ creation_date TIMESTAMPTZ,
+ change_date TIMESTAMPTZ,
+ sequence BIGINT,
+
+ template TEXT,
+ language TEXT,
+ key TEXT,
+ text TEXT,
+
+ PRIMARY KEY (aggregate_id, template, key, language)
+);
diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto
index d689aa4fbc..908946a344 100644
--- a/proto/zitadel/admin.proto
+++ b/proto/zitadel/admin.proto
@@ -141,7 +141,18 @@ service AdminService {
}
};
}
-
+
+ // Returns the default languages
+ rpc GetSupportedLanguages(GetSupportedLanguagesRequest) returns (GetSupportedLanguagesResponse) {
+ option (google.api.http) = {
+ get: "/languages";
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "iam.read";
+ };
+ }
+
//Checks whether an organisation exists by the given parameters
rpc IsOrgUnique(IsOrgUniqueRequest) returns (IsOrgUniqueResponse) {
option (google.api.http) = {
@@ -1509,8 +1520,19 @@ service AdminService {
};
}
- //Returns the custom text for initial message
+ //Returns the default text for initial message (translation file)
rpc GetDefaultInitMessageText(GetDefaultInitMessageTextRequest) returns (GetDefaultInitMessageTextResponse) {
+ option (google.api.http) = {
+ get: "/text/default/message/init/{language}";
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "iam.policy.read";
+ };
+ }
+
+ //Returns the custom text for initial message (overwritten in eventstore)
+ rpc GetCustomInitMessageText(GetCustomInitMessageTextRequest) returns (GetCustomInitMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/init/{language}";
};
@@ -1535,8 +1557,19 @@ service AdminService {
};
}
- //Returns the custom text for password reset message
+ //Returns the default text for password reset message (translation file)
rpc GetDefaultPasswordResetMessageText(GetDefaultPasswordResetMessageTextRequest) returns (GetDefaultPasswordResetMessageTextResponse) {
+ option (google.api.http) = {
+ get: "/text/deafult/message/passwordreset/{language}";
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "iam.policy.read";
+ };
+ }
+
+ //Returns the custom text for password reset message (overwritten in eventstore)
+ rpc GetCustomPasswordResetMessageText(GetCustomPasswordResetMessageTextRequest) returns (GetCustomPasswordResetMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/passwordreset/{language}";
};
@@ -1562,8 +1595,19 @@ service AdminService {
}
- //Returns the custom text for verify email message
+ //Returns the default text for verify email message (translation files)
rpc GetDefaultVerifyEmailMessageText(GetDefaultVerifyEmailMessageTextRequest) returns (GetDefaultVerifyEmailMessageTextResponse) {
+ option (google.api.http) = {
+ get: "/text/default/message/verifyemail/{language}";
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "iam.policy.read";
+ };
+ }
+
+ //Returns the custom text for verify email message (overwritten in eventstore)
+ rpc GetCustomVerifyEmailMessageText(GetCustomVerifyEmailMessageTextRequest) returns (GetCustomVerifyEmailMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/verifyemail/{language}";
};
@@ -1588,8 +1632,19 @@ service AdminService {
};
}
- //Returns the custom text for verify phone message
+ //Returns the default text for verify phone message (translation file)
rpc GetDefaultVerifyPhoneMessageText(GetDefaultVerifyPhoneMessageTextRequest) returns (GetDefaultVerifyPhoneMessageTextResponse) {
+ option (google.api.http) = {
+ get: "/text/default/message/verifyphone/{language}";
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "iam.policy.read";
+ };
+ }
+
+ //Returns the custom text for verify phone message
+ rpc GetCustomVerifyPhoneMessageText(GetCustomVerifyPhoneMessageTextRequest) returns (GetCustomVerifyPhoneMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/verifyphone/{language}";
};
@@ -1597,7 +1652,6 @@ service AdminService {
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read";
};
-
}
//Sets the default custom text for verify phone message
@@ -1615,8 +1669,19 @@ service AdminService {
};
}
- //Returns the custom text for domain claimed message
+ //Returns the default text for domain claimed message (translation file)
rpc GetDefaultDomainClaimedMessageText(GetDefaultDomainClaimedMessageTextRequest) returns (GetDefaultDomainClaimedMessageTextResponse) {
+ option (google.api.http) = {
+ get: "/text/default/message/domainclaimed/{language}";
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "iam.policy.read";
+ };
+ }
+
+ //Returns the custom text for domain claimed message (overwritten in eventstore)
+ rpc GetCustomDomainClaimedMessageText(GetCustomDomainClaimedMessageTextRequest) returns (GetCustomDomainClaimedMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/domainclaimed/{language}";
};
@@ -1980,6 +2045,13 @@ message HealthzRequest {}
//This is an empty response
message HealthzResponse {}
+//This is an empty request
+message GetSupportedLanguagesRequest {}
+
+message GetSupportedLanguagesResponse {
+ repeated string languages = 1;
+}
+
// if name or domain is already in use, org is not unique
message IsOrgUniqueRequest {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
@@ -3050,6 +3122,14 @@ message GetDefaultInitMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
+message GetCustomInitMessageTextRequest {
+ string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message GetCustomInitMessageTextResponse {
+ zitadel.text.v1.MessageCustomText custom_text = 1;
+}
+
message SetDefaultInitMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@@ -3078,6 +3158,14 @@ message GetDefaultPasswordResetMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
+message GetCustomPasswordResetMessageTextRequest {
+ string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message GetCustomPasswordResetMessageTextResponse {
+ zitadel.text.v1.MessageCustomText custom_text = 1;
+}
+
message SetDefaultPasswordResetMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@@ -3106,6 +3194,14 @@ message GetDefaultVerifyEmailMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
+message GetCustomVerifyEmailMessageTextRequest {
+ string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message GetCustomVerifyEmailMessageTextResponse {
+ zitadel.text.v1.MessageCustomText custom_text = 1;
+}
+
message SetDefaultVerifyEmailMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@@ -3134,6 +3230,14 @@ message GetDefaultVerifyPhoneMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
+message GetCustomVerifyPhoneMessageTextRequest {
+ string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message GetCustomVerifyPhoneMessageTextResponse {
+ zitadel.text.v1.MessageCustomText custom_text = 1;
+}
+
message SetDefaultVerifyPhoneMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@@ -3162,6 +3266,14 @@ message GetDefaultDomainClaimedMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
+message GetCustomDomainClaimedMessageTextRequest {
+ string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message GetCustomDomainClaimedMessageTextResponse {
+ zitadel.text.v1.MessageCustomText custom_text = 1;
+}
+
message SetDefaultDomainClaimedMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
diff --git a/proto/zitadel/auth.proto b/proto/zitadel/auth.proto
index a248d92935..b1d50daa2a 100644
--- a/proto/zitadel/auth.proto
+++ b/proto/zitadel/auth.proto
@@ -49,6 +49,17 @@ service AuthService {
};
}
+ // Returns the default languages
+ rpc GetSupportedLanguages(GetSupportedLanguagesRequest) returns (GetSupportedLanguagesResponse) {
+ option (google.api.http) = {
+ get: "/languages";
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "authenticated";
+ };
+ }
+
// Returns my full blown user
rpc GetMyUser(GetMyUserRequest) returns (GetMyUserResponse) {
option (google.api.http) = {
@@ -504,6 +515,14 @@ message HealthzRequest {}
//This is an empty response
message HealthzResponse {}
+//This is an empty request
+message GetSupportedLanguagesRequest {}
+
+//This is an empty response
+message GetSupportedLanguagesResponse {
+ repeated string languages = 1;
+}
+
//This is an empty request
// the request parameters are read from the token-header
message GetMyUserRequest {}
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index d21c82bd4e..4f409457c5 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -84,6 +84,17 @@ service ManagementService {
};
}
+ // Returns the default languages
+ rpc GetSupportedLanguages(GetSupportedLanguagesRequest) returns (GetSupportedLanguagesResponse) {
+ option (google.api.http) = {
+ get: "/languages";
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "authenticated";
+ };
+ }
+
// Returns the requested full blown user (human or machine)
rpc GetUserByID(GetUserByIDRequest) returns (GetUserByIDResponse) {
option (google.api.http) = {
@@ -2568,6 +2579,13 @@ message GetIAMResponse {
string iam_project_id = 2;
}
+//This is an empty request
+message GetSupportedLanguagesRequest {}
+
+message GetSupportedLanguagesResponse {
+ repeated string languages = 1;
+}
+
message GetUserByIDRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}