From 79db2478016eec373011d21f1b5306e1d5e76301 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 3 May 2022 15:58:38 +0200 Subject: [PATCH] feat: set default language on instance (#3594) --- cmd/admin/setup/03.go | 10 +++- cmd/admin/setup/steps.yaml | 1 + cmd/defaults.yaml | 1 + docs/docs/apis/proto/system.md | 1 + internal/api/grpc/admin/language.go | 19 +++--- .../api/grpc/system/instance_converter.go | 3 + internal/command/instance.go | 58 ++++++++++++++++++- internal/command/user_human.go | 3 - internal/command/user_human_test.go | 15 ----- internal/query/instance.go | 2 + .../instance/event_default_language.go | 4 +- proto/zitadel/system.proto | 1 + 12 files changed, 83 insertions(+), 35 deletions(-) diff --git a/cmd/admin/setup/03.go b/cmd/admin/setup/03.go index 0cef025978..36a0fcbc27 100644 --- a/cmd/admin/setup/03.go +++ b/cmd/admin/setup/03.go @@ -6,6 +6,8 @@ import ( "fmt" "strings" + "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/config/systemdefaults" @@ -15,9 +17,10 @@ import ( ) type DefaultInstance struct { - InstanceName string - CustomDomain string - Org command.OrgSetup + InstanceName string + CustomDomain string + DefaultLanguage language.Tag + Org command.OrgSetup instanceSetup command.InstanceSetup userEncryptionKey *crypto.KeyConfig @@ -67,6 +70,7 @@ func (mig *DefaultInstance) Execute(ctx context.Context) error { mig.instanceSetup.InstanceName = mig.InstanceName mig.instanceSetup.CustomDomain = mig.CustomDomain + mig.instanceSetup.DefaultLanguage = mig.DefaultLanguage mig.instanceSetup.Org = mig.Org mig.instanceSetup.Org.Human.Email.Address = strings.TrimSpace(mig.instanceSetup.Org.Human.Email.Address) if mig.instanceSetup.Org.Human.Email.Address == "" { diff --git a/cmd/admin/setup/steps.yaml b/cmd/admin/setup/steps.yaml index e81ce6ac77..b9e15dff37 100644 --- a/cmd/admin/setup/steps.yaml +++ b/cmd/admin/setup/steps.yaml @@ -1,6 +1,7 @@ S3DefaultInstance: InstanceName: Localhost CustomDomain: localhost + DefaultLanguage: en Org: Name: ZITADEL Human: diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 71ce49ed89..98aad55b56 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -162,6 +162,7 @@ SystemDefaults: DefaultInstance: InstanceName: + DefaultLanguage: en Org: Name: Human: diff --git a/docs/docs/apis/proto/system.md b/docs/docs/apis/proto/system.md index 83207df658..2d75c73df6 100644 --- a/docs/docs/apis/proto/system.md +++ b/docs/docs/apis/proto/system.md @@ -233,6 +233,7 @@ failed event. You can find out if it worked on the `failure_count` | owner_email | AddInstanceRequest.Email | - | message.required: true
| | owner_profile | AddInstanceRequest.Profile | - | message.required: false
| | owner_password | AddInstanceRequest.Password | - | message.required: false
| +| default_language | string | - | string.max_len: 10
| diff --git a/internal/api/grpc/admin/language.go b/internal/api/grpc/admin/language.go index 6e29463993..73924a401e 100644 --- a/internal/api/grpc/admin/language.go +++ b/internal/api/grpc/admin/language.go @@ -6,6 +6,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/api/grpc/text" caos_errors "github.com/zitadel/zitadel/internal/errors" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" @@ -20,19 +21,17 @@ func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSup } func (s *Server) SetDefaultLanguage(ctx context.Context, req *admin_pb.SetDefaultLanguageRequest) (*admin_pb.SetDefaultLanguageResponse, error) { - _, err := language.Parse(req.Language) + lang, err := language.Parse(req.Language) if err != nil { return nil, caos_errors.ThrowInvalidArgument(err, "API-39nnf", "Errors.Language.Parse") } - //TODO: Will be added by silvan - //details, err := s.command.SetDefaultLanguage(ctx, lang) - //if err != nil { - // return nil, err - //} - //return &admin_pb.SetDefaultLanguageResponse{ - // Details: object.DomainToChangeDetailsPb(details), - //}, nil - return nil, nil + details, err := s.command.SetDefaultLanguage(ctx, lang) + if err != nil { + return nil, err + } + return &admin_pb.SetDefaultLanguageResponse{ + Details: object.DomainToChangeDetailsPb(details), + }, nil } func (s *Server) GetDefaultLanguage(ctx context.Context, _ *admin_pb.GetDefaultLanguageRequest) (*admin_pb.GetDefaultLanguageResponse, error) { diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go index 6728b0eb43..fb812b62fe 100644 --- a/internal/api/grpc/system/instance_converter.go +++ b/internal/api/grpc/system/instance_converter.go @@ -47,6 +47,9 @@ func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInst defaultInstance.Org.Human.Password = req.OwnerPassword.Password defaultInstance.Org.Human.PasswordChangeRequired = req.OwnerPassword.PasswordChangeRequired } + if lang := language.Make(req.DefaultLanguage); lang != language.Und { + defaultInstance.DefaultLanguage = lang + } return &defaultInstance } diff --git a/internal/command/instance.go b/internal/command/instance.go index a1239bc6d1..6adcfd8860 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -4,6 +4,8 @@ import ( "context" "time" + "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/ui/console" "github.com/zitadel/zitadel/internal/command/preparation" @@ -32,6 +34,7 @@ type InstanceSetup struct { zitadel ZitadelConfig InstanceName string CustomDomain string + DefaultLanguage language.Tag Org OrgSetup SecretGenerators struct { PasswordSaltCost uint @@ -166,7 +169,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str projectAgg := project.NewAggregate(setup.zitadel.projectID, orgID) validations := []preparation.Validation{ - addInstance(instanceAgg, setup.InstanceName), + addInstance(instanceAgg, setup.InstanceName, setup.DefaultLanguage), addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeAppSecret, setup.SecretGenerators.ClientSecret), addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeInitCode, setup.SecretGenerators.InitializeUserCode), addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeVerifyEmailCode, setup.SecretGenerators.EmailVerificationCode), @@ -332,11 +335,30 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str }, nil } -func addInstance(a *instance.Aggregate, instanceName string) preparation.Validation { +func (c *Commands) SetDefaultLanguage(ctx context.Context, defaultLanguage language.Tag) (*domain.ObjectDetails, error) { + instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) + validation := c.prepareSetDefaultLanguage(instanceAgg, defaultLanguage) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) + if err != nil { + return nil, err + } + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().InstanceID, + }, nil +} + +func addInstance(a *instance.Aggregate, instanceName string, defaultLanguage language.Tag) preparation.Validation { return func() (preparation.CreateCommands, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return []eventstore.Command{ instance.NewInstanceAddedEvent(ctx, &a.Aggregate, instanceName), + instance.NewDefaultLanguageSetEvent(ctx, &a.Aggregate, defaultLanguage), }, nil }, nil } @@ -385,3 +407,35 @@ func (c *Commands) setIAMProject(ctx context.Context, iamAgg *eventstore.Aggrega } return instance.NewIAMProjectSetEvent(ctx, iamAgg, projectID), nil } + +func (c *Commands) prepareSetDefaultLanguage(a *instance.Aggregate, defaultLanguage language.Tag) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if defaultLanguage == language.Und { + return nil, errors.ThrowInvalidArgument(nil, "INST-28nlD", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := getInstanceWriteModel(ctx, filter) + if err != nil { + return nil, err + } + if writeModel.DefaultLanguage == defaultLanguage { + return nil, errors.ThrowPreconditionFailed(nil, "INST-DS3rq", "Errors.Instance.NotChanged") + } + return []eventstore.Command{instance.NewDefaultLanguageSetEvent(ctx, &a.Aggregate, defaultLanguage)}, nil + }, nil + } +} + +func getInstanceWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*InstanceWriteModel, error) { + writeModel := NewInstanceWriteModel(authz.GetInstance(ctx).InstanceID()) + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return writeModel, nil + } + writeModel.AppendEvents(events...) + err = writeModel.Reduce() + return writeModel, err +} diff --git a/internal/command/user_human.go b/internal/command/user_human.go index f2b65b8837..20a2d11aa9 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -98,9 +98,6 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument") } - if human.PreferredLanguage == language.Und { - return nil, errors.ThrowInvalidArgument(nil, "USER-Sfd11", "Errors.Invalid.Argument") - } if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" { return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument") } diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 9678aa9c6c..c0a7f4cf8d 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -2856,21 +2856,6 @@ func TestAddHumanCommand(t *testing.T) { ValidationErr: errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument"), }, }, - { - name: "invalid preferred language", - args: args{ - a: agg, - human: &AddHuman{ - Username: "username", - Email: Email{ - Address: "support@zitadel.ch", - }, - }, - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "USER-Sfd11", "Errors.Invalid.Argument"), - }, - }, { name: "invalid first name", args: args{ diff --git a/internal/query/instance.go b/internal/query/instance.go index dbb032101b..6fb356786d 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -281,6 +281,7 @@ func prepareInstancesQuery() (sq.SelectBuilder, func(*sql.Rows) (*Instances, err &lang, &count, ) + instance.DefaultLang = language.Make(lang) if err != nil { return nil, err } @@ -378,6 +379,7 @@ func prepareInstanceDomainQuery(host string) (sq.SelectBuilder, func(*sql.Rows) InstanceID: instance.ID, }) } + instance.DefaultLang = language.Make(lang) if err := rows.Close(); err != nil { return nil, errors.ThrowInternal(err, "QUERY-Dfbe2", "Errors.Query.CloseRows") } diff --git a/internal/repository/instance/event_default_language.go b/internal/repository/instance/event_default_language.go index 74ef81ee1d..a596a0a5ca 100644 --- a/internal/repository/instance/event_default_language.go +++ b/internal/repository/instance/event_default_language.go @@ -4,15 +4,15 @@ import ( "context" "encoding/json" - "github.com/zitadel/zitadel/internal/eventstore" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" ) const ( - DefaultLanguageSetEventType eventstore.EventType = "iam.default.language.set" + DefaultLanguageSetEventType eventstore.EventType = "instance.default.language.set" ) type DefaultLanguageSetEvent struct { diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index b18e8b4667..11d1b617cb 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -342,6 +342,7 @@ message AddInstanceRequest { Email owner_email = 5 [(validate.rules).message.required = true]; Profile owner_profile = 6 [(validate.rules).message.required = false]; Password owner_password = 7 [(validate.rules).message.required = false]; + string default_language = 8 [(validate.rules).string = {max_len: 10}]; } message AddInstanceResponse {