From a1d404291d365eb9ef71c15f2b8cc77abe295367 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 6 Jul 2022 14:09:49 +0200 Subject: [PATCH] fix(notify): notify user in projection (#3889) * start implement notify user in projection * fix(stmt): add copy to multi stmt * use projections for notify users * feat: notifications from projections * feat: notifications from projections * cleanup * pre-release * fix tests * fix types * fix command * fix queryNotifyUser * fix: build version * fix: HumanPasswordlessInitCodeSent Co-authored-by: adlerhurst --- .releaserc.js | 2 +- cmd/start/config.go | 2 - cmd/start/start.go | 2 +- internal/api/grpc/auth/user.go | 43 +- internal/api/grpc/management/user.go | 58 +- internal/command/user.go | 3 +- internal/command/user_membership.go | 30 +- internal/command/user_test.go | 13 +- internal/eventstore/handler/crdb/statement.go | 24 +- .../eventstore/handler/crdb/statement_test.go | 138 +++- internal/notification/notification.go | 38 - internal/notification/projection.go | 663 ++++++++++++++++++ .../eventsourcing/handler/handler.go | 89 --- .../eventsourcing/handler/notification.go | 637 ----------------- .../eventsourcing/handler/notify_user.go | 278 -------- .../repository/eventsourcing/repository.go | 56 -- .../repository/eventsourcing/spooler/lock.go | 20 - .../eventsourcing/spooler/spooler.go | 47 -- .../eventsourcing/view/error_event.go | 17 - .../eventsourcing/view/notification.go | 34 - .../eventsourcing/view/notify_user.go | 61 -- .../repository/eventsourcing/view/sequence.go | 38 - .../repository/eventsourcing/view/view.go | 25 - .../notification/repository/repository.go | 5 - .../notification/templates/templateData.go | 2 +- internal/notification/types/domain_claimed.go | 27 +- .../types/email_verification_code.go | 39 +- internal/notification/types/init_code.go | 45 +- internal/notification/types/notification.go | 73 ++ internal/notification/types/password_code.go | 47 +- .../types/passwordless_registration_link.go | 36 +- .../types/phone_verification_code.go | 36 +- internal/notification/types/templateData.go | 2 +- internal/notification/types/user_email.go | 37 +- internal/notification/types/user_phone.go | 13 +- internal/query/iam_member_test.go | 20 +- internal/query/org_member_test.go | 20 +- internal/query/project_grant_member_test.go | 20 +- internal/query/project_member_test.go | 20 +- internal/query/projection/label_policy.go | 26 + internal/query/projection/projection.go | 9 +- internal/query/projection/user.go | 134 +++- internal/query/projection/user_test.go | 258 +++++-- internal/query/user.go | 218 +++++- internal/query/user_grant_test.go | 40 +- internal/query/user_test.go | 412 ++++++++--- 46 files changed, 2018 insertions(+), 1839 deletions(-) delete mode 100644 internal/notification/notification.go create mode 100644 internal/notification/projection.go delete mode 100644 internal/notification/repository/eventsourcing/handler/handler.go delete mode 100644 internal/notification/repository/eventsourcing/handler/notification.go delete mode 100644 internal/notification/repository/eventsourcing/handler/notify_user.go delete mode 100644 internal/notification/repository/eventsourcing/repository.go delete mode 100644 internal/notification/repository/eventsourcing/spooler/lock.go delete mode 100644 internal/notification/repository/eventsourcing/spooler/spooler.go delete mode 100644 internal/notification/repository/eventsourcing/view/error_event.go delete mode 100644 internal/notification/repository/eventsourcing/view/notification.go delete mode 100644 internal/notification/repository/eventsourcing/view/notify_user.go delete mode 100644 internal/notification/repository/eventsourcing/view/sequence.go delete mode 100644 internal/notification/repository/eventsourcing/view/view.go delete mode 100644 internal/notification/repository/repository.go create mode 100644 internal/notification/types/notification.go diff --git a/.releaserc.js b/.releaserc.js index 309ca9e3a5..5c4725f1cc 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -3,7 +3,7 @@ module.exports = { {name: 'main'}, {name: '1.x.x', range: '1.x.x', channel: '1.x.x'}, {name: 'v2-alpha', prerelease: true}, - {name: 'update-projection-on-query', prerelease: true}, + {name: 'notify-users', prerelease: true}, ], plugins: [ "@semantic-release/commit-analyzer" diff --git a/cmd/start/config.go b/cmd/start/config.go index 179ab3ed4a..ad7d161704 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -20,7 +20,6 @@ import ( "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/notification" "github.com/zitadel/zitadel/internal/query/projection" static_config "github.com/zitadel/zitadel/internal/static/config" tracing "github.com/zitadel/zitadel/internal/telemetry/tracing/config" @@ -45,7 +44,6 @@ type Config struct { OIDC oidc.Config Login login.Config Console console.Config - Notification notification.Config AssetStorage static_config.AssetStorageConfig InternalAuthZ internal_authz.Config SystemDefaults systemdefaults.SystemDefaults diff --git a/cmd/start/start.go b/cmd/start/start.go index 1c2e2fdb4f..313ef460e3 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -139,7 +139,7 @@ func startZitadel(config *Config, masterKey string) error { return fmt.Errorf("cannot start commands: %w", err) } - notification.Start(config.Notification, config.ExternalPort, config.ExternalSecure, commands, queries, dbClient, assets.HandlerPrefix, config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, keys.SMS) + notification.Start(ctx, config.Projections.Customizations["notifications"], config.ExternalPort, config.ExternalSecure, commands, queries, eventstoreClient, assets.AssetAPI(config.ExternalSecure), config.SystemDefaults.Notifications.FileSystemPath, keys.User, keys.SMTP, keys.SMS) router := mux.NewRouter() tlsConfig, err := config.TLS.Config() diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index 0650082477..7f31b51567 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -9,6 +9,7 @@ import ( obj_grpc "github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/api/grpc/org" user_grpc "github.com/zitadel/zitadel/internal/api/grpc/user" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" @@ -45,7 +46,7 @@ func (s *Server) RemoveMyUser(ctx context.Context, _ *auth_pb.RemoveMyUserReques if err != nil { return nil, err } - details, err := s.command.RemoveUser(ctx, ctxData.UserID, ctxData.ResourceOwner, memberships.Memberships, userGrantsToIDs(grants.UserGrants)...) + details, err := s.command.RemoveUser(ctx, ctxData.UserID, ctxData.ResourceOwner, cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants)...) if err != nil { return nil, err } @@ -277,6 +278,46 @@ func MemberTypeToDomain(m *query.Membership) (_ domain.MemberType, displayName, return domain.MemberTypeUnspecified, "", "", "" } +func cascadingMemberships(memberships []*query.Membership) []*command.CascadingMembership { + cascades := make([]*command.CascadingMembership, len(memberships)) + for i, membership := range memberships { + cascades[i] = &command.CascadingMembership{ + UserID: membership.UserID, + ResourceOwner: membership.ResourceOwner, + IAM: cascadingIAMMembership(membership.IAM), + Org: cascadingOrgMembership(membership.Org), + Project: cascadingProjectMembership(membership.Project), + ProjectGrant: cascadingProjectGrantMembership(membership.ProjectGrant), + } + } + return cascades +} + +func cascadingIAMMembership(membership *query.IAMMembership) *command.CascadingIAMMembership { + if membership == nil { + return nil + } + return &command.CascadingIAMMembership{IAMID: membership.IAMID} +} +func cascadingOrgMembership(membership *query.OrgMembership) *command.CascadingOrgMembership { + if membership == nil { + return nil + } + return &command.CascadingOrgMembership{OrgID: membership.OrgID} +} +func cascadingProjectMembership(membership *query.ProjectMembership) *command.CascadingProjectMembership { + if membership == nil { + return nil + } + return &command.CascadingProjectMembership{ProjectID: membership.ProjectID} +} +func cascadingProjectGrantMembership(membership *query.ProjectGrantMembership) *command.CascadingProjectGrantMembership { + if membership == nil { + return nil + } + return &command.CascadingProjectGrantMembership{ProjectID: membership.ProjectID, GrantID: membership.GrantID} +} + func userGrantsToIDs(userGrants []*query.UserGrant) []string { converted := make([]string, len(userGrants)) for i, grant := range userGrants { diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index ef58b87f33..5fd3bc7822 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -338,7 +338,7 @@ func (s *Server) RemoveUser(ctx context.Context, req *mgmt_pb.RemoveUserRequest) if err != nil { return nil, err } - objectDetails, err := s.command.RemoveUser(ctx, req.Id, authz.GetCtxData(ctx).OrgID, memberships.Memberships, userGrantsToIDs(grants.UserGrants)...) + objectDetails, err := s.command.RemoveUser(ctx, req.Id, authz.GetCtxData(ctx).OrgID, cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants)...) if err != nil { return nil, err } @@ -347,14 +347,6 @@ func (s *Server) RemoveUser(ctx context.Context, req *mgmt_pb.RemoveUserRequest) }, nil } -func userGrantsToIDs(userGrants []*query.UserGrant) []string { - converted := make([]string, len(userGrants)) - for i, grant := range userGrants { - converted[i] = grant.ID - } - return converted -} - func (s *Server) UpdateUserName(ctx context.Context, req *mgmt_pb.UpdateUserNameRequest) (*mgmt_pb.UpdateUserNameResponse, error) { objectDetails, err := s.command.ChangeUsername(ctx, authz.GetCtxData(ctx).OrgID, req.UserId, req.UserName) if err != nil { @@ -860,3 +852,51 @@ func (s *Server) ListUserMemberships(ctx context.Context, req *mgmt_pb.ListUserM Details: obj_grpc.ToListDetails(response.Count, response.Sequence, response.Timestamp), }, nil } + +func cascadingMemberships(memberships []*query.Membership) []*command.CascadingMembership { + cascades := make([]*command.CascadingMembership, len(memberships)) + for i, membership := range memberships { + cascades[i] = &command.CascadingMembership{ + UserID: membership.UserID, + ResourceOwner: membership.ResourceOwner, + IAM: cascadingIAMMembership(membership.IAM), + Org: cascadingOrgMembership(membership.Org), + Project: cascadingProjectMembership(membership.Project), + ProjectGrant: cascadingProjectGrantMembership(membership.ProjectGrant), + } + } + return cascades +} + +func cascadingIAMMembership(membership *query.IAMMembership) *command.CascadingIAMMembership { + if membership == nil { + return nil + } + return &command.CascadingIAMMembership{IAMID: membership.IAMID} +} +func cascadingOrgMembership(membership *query.OrgMembership) *command.CascadingOrgMembership { + if membership == nil { + return nil + } + return &command.CascadingOrgMembership{OrgID: membership.OrgID} +} +func cascadingProjectMembership(membership *query.ProjectMembership) *command.CascadingProjectMembership { + if membership == nil { + return nil + } + return &command.CascadingProjectMembership{ProjectID: membership.ProjectID} +} +func cascadingProjectGrantMembership(membership *query.ProjectGrantMembership) *command.CascadingProjectGrantMembership { + if membership == nil { + return nil + } + return &command.CascadingProjectGrantMembership{ProjectID: membership.ProjectID, GrantID: membership.GrantID} +} + +func userGrantsToIDs(userGrants []*query.UserGrant) []string { + converted := make([]string, len(userGrants)) + for i, grant := range userGrants { + converted[i] = grant.ID + } + return converted +} diff --git a/internal/command/user.go b/internal/command/user.go index be5b8bec94..a4f33a1fb5 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -14,7 +14,6 @@ import ( caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" - "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -174,7 +173,7 @@ func (c *Commands) UnlockUser(ctx context.Context, userID, resourceOwner string) return writeModelToObjectDetails(&existingUser.WriteModel), nil } -func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string, cascadingUserMemberships []*query.Membership, cascadingGrantIDs ...string) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string, cascadingUserMemberships []*CascadingMembership, cascadingGrantIDs ...string) (*domain.ObjectDetails, error) { if userID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing") } diff --git a/internal/command/user_membership.go b/internal/command/user_membership.go index db1831f4ea..3bc68dd4be 100644 --- a/internal/command/user_membership.go +++ b/internal/command/user_membership.go @@ -4,13 +4,39 @@ import ( "context" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/project" ) -func (c *Commands) removeUserMemberships(ctx context.Context, memberships []*query.Membership) (_ []eventstore.Command, err error) { +type CascadingMembership struct { + UserID string + ResourceOwner string + + IAM *CascadingIAMMembership + Org *CascadingOrgMembership + Project *CascadingProjectMembership + ProjectGrant *CascadingProjectGrantMembership +} + +type CascadingIAMMembership struct { + IAMID string +} + +type CascadingOrgMembership struct { + OrgID string +} + +type CascadingProjectMembership struct { + ProjectID string +} + +type CascadingProjectGrantMembership struct { + ProjectID string + GrantID string +} + +func (c *Commands) removeUserMemberships(ctx context.Context, memberships []*CascadingMembership) (_ []eventstore.Command, err error) { events := make([]eventstore.Command, 0) for _, membership := range memberships { if membership.IAM != nil { diff --git a/internal/command/user_test.go b/internal/command/user_test.go index 3d8c347442..383d7205df 100644 --- a/internal/command/user_test.go +++ b/internal/command/user_test.go @@ -19,7 +19,6 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" "github.com/zitadel/zitadel/internal/id" - "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/user" ) @@ -929,7 +928,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { instanceID string orgID string userID string - cascadeUserMemberships []*query.Membership + cascadeUserMemberships []*CascadingMembership cascadeUserGrants []string } ) @@ -1215,16 +1214,16 @@ func TestCommandSide_RemoveUser(t *testing.T) { ctx: context.Background(), orgID: "org1", userID: "user1", - cascadeUserMemberships: []*query.Membership{ + cascadeUserMemberships: []*CascadingMembership{ { - IAM: &query.IAMMembership{ + IAM: &CascadingIAMMembership{ IAMID: "INSTANCE", }, UserID: "user1", ResourceOwner: "org1", }, { - Org: &query.OrgMembership{ + Org: &CascadingOrgMembership{ OrgID: "org1", }, UserID: "user1", @@ -1232,14 +1231,14 @@ func TestCommandSide_RemoveUser(t *testing.T) { }, { - Project: &query.ProjectMembership{ + Project: &CascadingProjectMembership{ ProjectID: "project1", }, UserID: "user1", ResourceOwner: "org1", }, { - ProjectGrant: &query.ProjectGrantMembership{ + ProjectGrant: &CascadingProjectGrantMembership{ ProjectID: "project1", GrantID: "grant1", }, diff --git a/internal/eventstore/handler/crdb/statement.go b/internal/eventstore/handler/crdb/statement.go index 5b544c6909..c4bc2db9d5 100644 --- a/internal/eventstore/handler/crdb/statement.go +++ b/internal/eventstore/handler/crdb/statement.go @@ -189,6 +189,12 @@ func AddDeleteStatement(conditions []handler.Condition, opts ...execOption) func } } +func AddCopyStatement(from, to []handler.Column, conditions []handler.Condition, opts ...execOption) func(eventstore.Event) Exec { + return func(event eventstore.Event) Exec { + return NewCopyStatement(event, from, to, conditions, opts...).Execute + } +} + func NewArrayAppendCol(column string, value interface{}) handler.Column { return handler.Column{ Name: column, @@ -233,19 +239,19 @@ func NewArrayIntersectCol(column string, value interface{}) handler.Column { // if the value of a col is empty the data will be copied from the selected row // if the value of a col is not empty the data will be set by the static value // conds represent the conditions for the selection subquery -func NewCopyStatement(event eventstore.Event, cols []handler.Column, conds []handler.Condition, opts ...execOption) *handler.Statement { - columnNames := make([]string, len(cols)) - selectColumns := make([]string, len(cols)) +func NewCopyStatement(event eventstore.Event, from, to []handler.Column, conds []handler.Condition, opts ...execOption) *handler.Statement { + columnNames := make([]string, len(to)) + selectColumns := make([]string, len(from)) argCounter := 0 args := []interface{}{} - for i, col := range cols { - columnNames[i] = col.Name - selectColumns[i] = col.Name - if col.Value != nil { + for i := range from { + columnNames[i] = to[i].Name + selectColumns[i] = from[i].Name + if from[i].Value != nil { argCounter++ selectColumns[i] = "$" + strconv.Itoa(argCounter) - args = append(args, col.Value) + args = append(args, from[i].Value) } } @@ -260,7 +266,7 @@ func NewCopyStatement(event eventstore.Event, cols []handler.Column, conds []han args: args, } - if len(cols) == 0 { + if len(from) == 0 || len(to) == 0 || len(from) != len(to) { config.err = handler.ErrNoValues } diff --git a/internal/eventstore/handler/crdb/statement_test.go b/internal/eventstore/handler/crdb/statement_test.go index 8b5e33f266..a4bea29261 100644 --- a/internal/eventstore/handler/crdb/statement_test.go +++ b/internal/eventstore/handler/crdb/statement_test.go @@ -801,7 +801,8 @@ func TestNewCopyStatement(t *testing.T) { type args struct { table string event *testEvent - cols []handler.Column + from []handler.Column + to []handler.Column conds []handler.Condition } type want struct { @@ -856,7 +857,12 @@ func TestNewCopyStatement(t *testing.T) { previousSequence: 0, }, conds: []handler.Condition{}, - cols: []handler.Column{ + from: []handler.Column{ + { + Name: "col", + }, + }, + to: []handler.Column{ { Name: "col", }, @@ -876,7 +882,44 @@ func TestNewCopyStatement(t *testing.T) { }, }, { - name: "no values", + name: "more to than from cols", + args: args{ + table: "my_table", + event: &testEvent{ + aggregateType: "agg", + sequence: 1, + previousSequence: 0, + }, + conds: []handler.Condition{}, + from: []handler.Column{ + { + Name: "col", + }, + }, + to: []handler.Column{ + { + Name: "col", + }, + { + Name: "col2", + }, + }, + }, + want: want{ + table: "my_table", + aggregateType: "agg", + sequence: 1, + previousSequence: 1, + executer: &wantExecuter{ + shouldExecute: false, + }, + isErr: func(err error) bool { + return errors.Is(err, handler.ErrNoCondition) + }, + }, + }, + { + name: "no columns", args: args{ table: "my_table", event: &testEvent{ @@ -889,7 +932,7 @@ func TestNewCopyStatement(t *testing.T) { Name: "col", }, }, - cols: []handler.Column{}, + from: []handler.Column{}, }, want: want{ table: "my_table", @@ -905,7 +948,7 @@ func TestNewCopyStatement(t *testing.T) { }, }, { - name: "correct", + name: "correct same column names", args: args{ table: "my_table", event: &testEvent{ @@ -913,7 +956,7 @@ func TestNewCopyStatement(t *testing.T) { sequence: 1, previousSequence: 0, }, - cols: []handler.Column{ + from: []handler.Column{ { Name: "state", Value: 1, @@ -928,6 +971,20 @@ func TestNewCopyStatement(t *testing.T) { Name: "col_b", }, }, + to: []handler.Column{ + { + Name: "state", + }, + { + Name: "id", + }, + { + Name: "col_a", + }, + { + Name: "col_b", + }, + }, conds: []handler.Condition{ { Name: "id", @@ -958,11 +1015,78 @@ func TestNewCopyStatement(t *testing.T) { }, }, }, + { + name: "correct different column names", + args: args{ + table: "my_table", + event: &testEvent{ + aggregateType: "agg", + sequence: 1, + previousSequence: 0, + }, + from: []handler.Column{ + { + Value: 1, + }, + { + Name: "id", + }, + { + Name: "col_a", + }, + { + Name: "col_b", + }, + }, + to: []handler.Column{ + { + Name: "state", + }, + { + Name: "id", + }, + { + Name: "col_c", + }, + { + Name: "col_d", + }, + }, + conds: []handler.Condition{ + { + Name: "id", + Value: 2, + }, + { + Name: "state", + Value: 3, + }, + }, + }, + want: want{ + table: "my_table", + aggregateType: "agg", + sequence: 1, + previousSequence: 1, + executer: &wantExecuter{ + params: []params{ + { + query: "UPSERT INTO my_table (state, id, col_c, col_d) SELECT $1, id, col_a, col_b FROM my_table AS copy_table WHERE copy_table.id = $2 AND copy_table.state = $3", + args: []interface{}{1, 2, 3}, + }, + }, + shouldExecute: true, + }, + isErr: func(err error) bool { + return err == nil + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.want.executer.t = t - stmt := NewCopyStatement(tt.args.event, tt.args.cols, tt.args.conds) + stmt := NewCopyStatement(tt.args.event, tt.args.from, tt.args.to, tt.args.conds) err := stmt.Execute(tt.want.executer, tt.args.table) if !tt.want.isErr(err) { diff --git a/internal/notification/notification.go b/internal/notification/notification.go deleted file mode 100644 index 2475279bfd..0000000000 --- a/internal/notification/notification.go +++ /dev/null @@ -1,38 +0,0 @@ -package notification - -import ( - "database/sql" - - "github.com/rakyll/statik/fs" - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/crypto" - - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/notification/repository/eventsourcing" - _ "github.com/zitadel/zitadel/internal/notification/statik" - "github.com/zitadel/zitadel/internal/query" -) - -type Config struct { - Repository eventsourcing.Config -} - -func Start(config Config, - externalPort uint16, - externalSecure bool, - command *command.Commands, - queries *query.Queries, - dbClient *sql.DB, - assetsPrefix, - fileSystemPath string, - userEncryption crypto.EncryptionAlgorithm, - smtpEncryption crypto.EncryptionAlgorithm, - smsEncryption crypto.EncryptionAlgorithm, -) { - statikFS, err := fs.NewWithNamespace("notification") - logging.OnError(err).Panic("unable to start listener") - - _, err = eventsourcing.Start(config.Repository, statikFS, externalPort, externalSecure, command, queries, dbClient, assetsPrefix, fileSystemPath, userEncryption, smtpEncryption, smsEncryption) - logging.OnError(err).Panic("unable to start app") -} diff --git a/internal/notification/projection.go b/internal/notification/projection.go new file mode 100644 index 0000000000..17452e2d99 --- /dev/null +++ b/internal/notification/projection.go @@ -0,0 +1,663 @@ +package notification + +import ( + "context" + "net/http" + "time" + + statik_fs "github.com/rakyll/statik/fs" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" + http_utils "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" + "github.com/zitadel/zitadel/internal/i18n" + "github.com/zitadel/zitadel/internal/notification/channels/fs" + "github.com/zitadel/zitadel/internal/notification/channels/log" + "github.com/zitadel/zitadel/internal/notification/channels/smtp" + "github.com/zitadel/zitadel/internal/notification/channels/twilio" + _ "github.com/zitadel/zitadel/internal/notification/statik" + "github.com/zitadel/zitadel/internal/notification/types" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/repository/user" +) + +const ( + NotificationsProjectionTable = "projections.notifications" + NotifyUserID = "NOTIFICATION" //TODO: system? +) + +func Start(ctx context.Context, customConfig projection.CustomConfig, externalPort uint16, externalSecure bool, commands *command.Commands, queries *query.Queries, es *eventstore.Eventstore, assetsPrefix func(context.Context) string, fileSystemPath string, userEncryption, smtpEncryption, smsEncryption crypto.EncryptionAlgorithm) { + statikFS, err := statik_fs.NewWithNamespace("notification") + logging.OnError(err).Panic("unable to start listener") + + projection.NotificationsProjection = newNotificationsProjection(ctx, projection.ApplyCustomConfig(customConfig), commands, queries, es, userEncryption, smtpEncryption, smsEncryption, externalSecure, externalPort, fileSystemPath, assetsPrefix, statikFS) +} + +type notificationsProjection struct { + crdb.StatementHandler + commands *command.Commands + queries *query.Queries + es *eventstore.Eventstore + userDataCrypto crypto.EncryptionAlgorithm + smtpPasswordCrypto crypto.EncryptionAlgorithm + smsTokenCrypto crypto.EncryptionAlgorithm + assetsPrefix func(context.Context) string + fileSystemPath string + externalPort uint16 + externalSecure bool + statikDir http.FileSystem +} + +func newNotificationsProjection( + ctx context.Context, + config crdb.StatementHandlerConfig, + commands *command.Commands, + queries *query.Queries, + es *eventstore.Eventstore, + userDataCrypto, + smtpPasswordCrypto, + smsTokenCrypto crypto.EncryptionAlgorithm, + externalSecure bool, + externalPort uint16, + fileSystemPath string, + assetsPrefix func(context.Context) string, + statikDir http.FileSystem, +) *notificationsProjection { + p := new(notificationsProjection) + config.ProjectionName = NotificationsProjectionTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + p.commands = commands + p.queries = queries + p.es = es + p.userDataCrypto = userDataCrypto + p.smtpPasswordCrypto = smtpPasswordCrypto + p.smsTokenCrypto = smsTokenCrypto + p.assetsPrefix = assetsPrefix + p.externalPort = externalPort + p.externalSecure = externalSecure + p.fileSystemPath = fileSystemPath + p.statikDir = statikDir + + return p +} + +func (p *notificationsProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: user.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: user.UserV1InitialCodeAddedType, + Reduce: p.reduceInitCodeAdded, + }, + { + Event: user.HumanInitialCodeAddedType, + Reduce: p.reduceInitCodeAdded, + }, + { + Event: user.UserV1EmailCodeAddedType, + Reduce: p.reduceEmailCodeAdded, + }, + { + Event: user.HumanEmailCodeAddedType, + Reduce: p.reduceEmailCodeAdded, + }, + { + Event: user.UserV1PasswordCodeAddedType, + Reduce: p.reducePasswordCodeAdded, + }, + { + Event: user.HumanPasswordCodeAddedType, + Reduce: p.reducePasswordCodeAdded, + }, + { + Event: user.UserDomainClaimedType, + Reduce: p.reduceDomainClaimed, + }, + { + Event: user.HumanPasswordlessInitCodeRequestedType, + Reduce: p.reducePasswordlessCodeRequested, + }, + { + Event: user.UserV1PhoneCodeAddedType, + Reduce: p.reducePhoneCodeAdded, + }, + { + Event: user.HumanPhoneCodeAddedType, + Reduce: p.reducePhoneCodeAdded, + }, + }, + }, + } +} + +func (p *notificationsProjection) reduceInitCodeAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.HumanInitialCodeAddedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-EFe2f", "reduce.wrong.event.type %s", user.HumanInitialCodeAddedType) + } + ctx := setNotificationContext(event.Aggregate()) + alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, + user.UserV1InitialCodeAddedType, user.UserV1InitialCodeSentType, + user.HumanInitialCodeAddedType, user.HumanInitialCodeSentType) + if err != nil { + return nil, err + } + if alreadyHandled { + return crdb.NewNoOpStatement(e), nil + } + code, err := crypto.DecryptString(e.Code, p.userDataCrypto) + if err != nil { + return nil, err + } + colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + if err != nil { + return nil, err + } + translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.InitCodeMessageType) + if err != nil { + return nil, err + } + + origin, err := p.origin(ctx) + if err != nil { + return nil, err + } + err = types.SendEmail( + ctx, + string(template.Template), + translator, + notifyUser, + p.getSMTPConfig, + p.getFileSystemProvider, + p.getLogProvider, + colors, + p.assetsPrefix(ctx), + ).SendUserInitCode(notifyUser, origin, code) + if err != nil { + return nil, err + } + err = p.commands.HumanInitCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + if err != nil { + return nil, err + } + return crdb.NewNoOpStatement(e), nil +} + +func (p *notificationsProjection) reduceEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.HumanEmailCodeAddedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType) + } + ctx := setNotificationContext(event.Aggregate()) + alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, + user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType, + user.HumanEmailCodeAddedType, user.HumanEmailCodeSentType) + if err != nil { + return nil, err + } + if alreadyHandled { + return crdb.NewNoOpStatement(e), nil + } + code, err := crypto.DecryptString(e.Code, p.userDataCrypto) + if err != nil { + return nil, err + } + colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + if err != nil { + return nil, err + } + translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyEmailMessageType) + if err != nil { + return nil, err + } + + origin, err := p.origin(ctx) + if err != nil { + return nil, err + } + err = types.SendEmail( + ctx, + string(template.Template), + translator, + notifyUser, + p.getSMTPConfig, + p.getFileSystemProvider, + p.getLogProvider, + colors, + p.assetsPrefix(ctx), + ).SendEmailVerificationCode(notifyUser, origin, code) + if err != nil { + return nil, err + } + err = p.commands.HumanEmailVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + if err != nil { + return nil, err + } + return crdb.NewNoOpStatement(e), nil +} + +func (p *notificationsProjection) reducePasswordCodeAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.HumanPasswordCodeAddedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Eeg3s", "reduce.wrong.event.type %s", user.HumanPasswordCodeAddedType) + } + ctx := setNotificationContext(event.Aggregate()) + alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, + user.UserV1PasswordCodeAddedType, user.UserV1PasswordCodeSentType, + user.HumanPasswordCodeAddedType, user.HumanPasswordCodeSentType) + if err != nil { + return nil, err + } + if alreadyHandled { + return crdb.NewNoOpStatement(e), nil + } + code, err := crypto.DecryptString(e.Code, p.userDataCrypto) + if err != nil { + return nil, err + } + colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + if err != nil { + return nil, err + } + translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordResetMessageType) + if err != nil { + return nil, err + } + + origin, err := p.origin(ctx) + if err != nil { + return nil, err + } + notify := types.SendEmail( + ctx, + string(template.Template), + translator, + notifyUser, + p.getSMTPConfig, + p.getFileSystemProvider, + p.getLogProvider, + colors, + p.assetsPrefix(ctx), + ) + if e.NotificationType == domain.NotificationTypeSms { + notify = types.SendSMSTwilio( + ctx, + translator, + notifyUser, + p.getTwilioConfig, + p.getFileSystemProvider, + p.getLogProvider, + colors, + p.assetsPrefix(ctx), + ) + } + err = notify.SendPasswordCode(notifyUser, origin, code) + if err != nil { + return nil, err + } + err = p.commands.PasswordCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + if err != nil { + return nil, err + } + return crdb.NewNoOpStatement(e), nil +} + +func (p *notificationsProjection) reduceDomainClaimed(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.DomainClaimedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Drh5w", "reduce.wrong.event.type %s", user.UserDomainClaimedType) + } + ctx := setNotificationContext(event.Aggregate()) + alreadyHandled, err := p.checkIfAlreadyHandled(ctx, event, nil, + user.UserDomainClaimedType, user.UserDomainClaimedSentType) + if err != nil { + return nil, err + } + if alreadyHandled { + return crdb.NewNoOpStatement(e), nil + } + colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + if err != nil { + return nil, err + } + translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.DomainClaimedMessageType) + if err != nil { + return nil, err + } + + origin, err := p.origin(ctx) + if err != nil { + return nil, err + } + err = types.SendEmail( + ctx, + string(template.Template), + translator, + notifyUser, + p.getSMTPConfig, + p.getFileSystemProvider, + p.getLogProvider, + colors, + p.assetsPrefix(ctx), + ).SendDomainClaimed(notifyUser, origin, e.UserName) + if err != nil { + return nil, err + } + err = p.commands.UserDomainClaimedSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + if err != nil { + return nil, err + } + return crdb.NewNoOpStatement(e), nil +} + +func (p *notificationsProjection) reducePasswordlessCodeRequested(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.HumanPasswordlessInitCodeRequestedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-EDtjd", "reduce.wrong.event.type %s", user.HumanPasswordlessInitCodeAddedType) + } + ctx := setNotificationContext(event.Aggregate()) + alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, map[string]interface{}{"id": e.ID}, user.HumanPasswordlessInitCodeSentType) + if err != nil { + return nil, err + } + if alreadyHandled { + return crdb.NewNoOpStatement(e), nil + } + code, err := crypto.DecryptString(e.Code, p.userDataCrypto) + if err != nil { + return nil, err + } + colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + if err != nil { + return nil, err + } + translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordlessRegistrationMessageType) + if err != nil { + return nil, err + } + + origin, err := p.origin(ctx) + if err != nil { + return nil, err + } + err = types.SendEmail( + ctx, + string(template.Template), + translator, + notifyUser, + p.getSMTPConfig, + p.getFileSystemProvider, + p.getLogProvider, + colors, + p.assetsPrefix(ctx), + ).SendPasswordlessRegistrationLink(notifyUser, origin, code, e.ID) + if err != nil { + return nil, err + } + err = p.commands.HumanPasswordlessInitCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.ID) + if err != nil { + return nil, err + } + return crdb.NewNoOpStatement(e), nil +} + +func (p *notificationsProjection) reducePhoneCodeAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.HumanPhoneCodeAddedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-He83g", "reduce.wrong.event.type %s", user.HumanPhoneCodeAddedType) + } + ctx := setNotificationContext(event.Aggregate()) + alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, + user.UserV1PhoneCodeAddedType, user.UserV1PhoneCodeSentType, + user.HumanPhoneCodeAddedType, user.HumanPhoneCodeSentType) + if err != nil { + return nil, err + } + if alreadyHandled { + return crdb.NewNoOpStatement(e), nil + } + code, err := crypto.DecryptString(e.Code, p.userDataCrypto) + if err != nil { + return nil, err + } + colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner) + if err != nil { + return nil, err + } + + notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + if err != nil { + return nil, err + } + translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyPhoneMessageType) + if err != nil { + return nil, err + } + + origin, err := p.origin(ctx) + if err != nil { + return nil, err + } + err = types.SendSMSTwilio( + ctx, + translator, + notifyUser, + p.getTwilioConfig, + p.getFileSystemProvider, + p.getLogProvider, + colors, + p.assetsPrefix(ctx), + ).SendPhoneVerificationCode(notifyUser, origin, code) + if err != nil { + return nil, err + } + err = p.commands.HumanPhoneVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID) + if err != nil { + return nil, err + } + return crdb.NewNoOpStatement(e), nil +} + +func (p *notificationsProjection) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event eventstore.Event, expiry time.Duration, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) { + if event.CreationDate().Add(expiry).Before(time.Now().UTC()) { + return true, nil + } + return p.checkIfAlreadyHandled(ctx, event, data, eventTypes...) +} + +func (p *notificationsProjection) checkIfAlreadyHandled(ctx context.Context, event eventstore.Event, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) { + events, err := p.es.Filter( + ctx, + eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + InstanceID(event.Aggregate().InstanceID). + AddQuery(). + AggregateTypes(user.AggregateType). + AggregateIDs(event.Aggregate().ID). + SequenceGreater(event.Sequence()). + EventTypes(eventTypes...). + EventData(data). + Builder(), + ) + if err != nil { + return false, err + } + return len(events) > 0, nil +} +func (p *notificationsProjection) getSMTPConfig(ctx context.Context) (*smtp.EmailConfig, error) { + config, err := p.queries.SMTPConfigByAggregateID(ctx, authz.GetInstance(ctx).InstanceID()) + if err != nil { + return nil, err + } + password, err := crypto.DecryptString(config.Password, p.smtpPasswordCrypto) + if err != nil { + return nil, err + } + return &smtp.EmailConfig{ + From: config.SenderAddress, + FromName: config.SenderName, + Tls: config.TLS, + SMTP: smtp.SMTP{ + Host: config.Host, + User: config.User, + Password: password, + }, + }, nil +} + +// Read iam twilio config +func (p *notificationsProjection) getTwilioConfig(ctx context.Context) (*twilio.TwilioConfig, error) { + active, err := query.NewSMSProviderStateQuery(domain.SMSConfigStateActive) + if err != nil { + return nil, err + } + config, err := p.queries.SMSProviderConfig(ctx, active) + if err != nil { + return nil, err + } + if config.TwilioConfig == nil { + return nil, errors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound") + } + token, err := crypto.DecryptString(config.TwilioConfig.Token, p.smsTokenCrypto) + if err != nil { + return nil, err + } + return &twilio.TwilioConfig{ + SID: config.TwilioConfig.SID, + Token: token, + SenderNumber: config.TwilioConfig.SenderNumber, + }, nil +} + +// Read iam filesystem provider config +func (p *notificationsProjection) getFileSystemProvider(ctx context.Context) (*fs.FSConfig, error) { + config, err := p.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeFile) + if err != nil { + return nil, err + } + return &fs.FSConfig{ + Compact: config.Compact, + Path: p.fileSystemPath, + }, nil +} + +// Read iam log provider config +func (p *notificationsProjection) getLogProvider(ctx context.Context) (*log.LogConfig, error) { + config, err := p.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeLog) + if err != nil { + return nil, err + } + return &log.LogConfig{ + Compact: config.Compact, + }, nil +} + +func (p *notificationsProjection) getTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) { + translator, err := i18n.NewTranslator(p.statikDir, p.queries.GetDefaultLanguage(ctx), "") + if err != nil { + return nil, err + } + + allCustomTexts, err := p.queries.CustomTextListByTemplate(ctx, authz.GetInstance(ctx).InstanceID(), textType) + if err != nil { + return translator, nil + } + customTexts, err := p.queries.CustomTextListByTemplate(ctx, orgID, textType) + if err != nil { + return translator, nil + } + allCustomTexts.CustomTexts = append(allCustomTexts.CustomTexts, customTexts.CustomTexts...) + + for _, text := range allCustomTexts.CustomTexts { + msg := i18n.Message{ + ID: text.Template + "." + text.Key, + Text: text.Text, + } + err = translator.AddMessages(text.Language, msg) + logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "orgID", orgID, "messageType", textType, "messageID", msg.ID). + OnError(err). + Warn("could not add translation message") + } + return translator, nil +} + +func (p *notificationsProjection) origin(ctx context.Context) (string, error) { + primary, err := query.NewInstanceDomainPrimarySearchQuery(true) + if err != nil { + return "", err + } + domains, err := p.queries.SearchInstanceDomains(ctx, &query.InstanceDomainSearchQueries{ + Queries: []query.SearchQuery{primary}, + }) + if err != nil { + return "", err + } + if len(domains.Domains) < 1 { + return "", errors.ThrowInternal(nil, "NOTIF-Ef3r1", "Errors.Notification.NoDomain") + } + return http_utils.BuildHTTP(domains.Domains[0].Domain, p.externalPort, p.externalSecure), nil +} + +func setNotificationContext(event eventstore.Aggregate) context.Context { + ctx := authz.WithInstanceID(context.Background(), event.InstanceID) + return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: event.ResourceOwner}) +} diff --git a/internal/notification/repository/eventsourcing/handler/handler.go b/internal/notification/repository/eventsourcing/handler/handler.go deleted file mode 100644 index 303757f81d..0000000000 --- a/internal/notification/repository/eventsourcing/handler/handler.go +++ /dev/null @@ -1,89 +0,0 @@ -package handler - -import ( - "net/http" - "time" - - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/crypto" - v1 "github.com/zitadel/zitadel/internal/eventstore/v1" - queryv1 "github.com/zitadel/zitadel/internal/eventstore/v1/query" - "github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/view" - "github.com/zitadel/zitadel/internal/query" -) - -type Configs map[string]*Config - -type Config struct { - MinimumCycleDuration time.Duration -} - -type handler struct { - view *view.View - bulkLimit uint64 - cycleDuration time.Duration - errorCountUntilSkip uint64 - - es v1.Eventstore -} - -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, - queries *query.Queries, - externalPort uint16, - externalSecure bool, - dir http.FileSystem, - assetsPrefix, - fileSystemPath string, - userEncryption crypto.EncryptionAlgorithm, - smtpEncryption crypto.EncryptionAlgorithm, - smsEncryption crypto.EncryptionAlgorithm, -) []queryv1.Handler { - return []queryv1.Handler{ - newNotifyUser( - handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es}, - queries, - ), - newNotification( - handler{view, bulkLimit, configs.cycleDuration("Notification"), errorCount, es}, - command, - queries, - externalPort, - externalSecure, - dir, - assetsPrefix, - fileSystemPath, - userEncryption, - smtpEncryption, - smsEncryption, - ), - } -} - -func (configs Configs) cycleDuration(viewModel string) time.Duration { - c, ok := configs[viewModel] - if !ok { - return 1 * time.Minute - } - return c.MinimumCycleDuration -} - -func (h *handler) MinimumCycleDuration() time.Duration { - return h.cycleDuration -} - -func (h *handler) LockDuration() time.Duration { - return h.cycleDuration / 3 -} - -func (h *handler) QueryLimit() uint64 { - return h.bulkLimit -} diff --git a/internal/notification/repository/eventsourcing/handler/notification.go b/internal/notification/repository/eventsourcing/handler/notification.go deleted file mode 100644 index df066d191b..0000000000 --- a/internal/notification/repository/eventsourcing/handler/notification.go +++ /dev/null @@ -1,637 +0,0 @@ -package handler - -import ( - "context" - "encoding/json" - "net/http" - "time" - - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/api/authz" - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/errors" - "github.com/zitadel/zitadel/internal/eventstore" - v1 "github.com/zitadel/zitadel/internal/eventstore/v1" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - queryv1 "github.com/zitadel/zitadel/internal/eventstore/v1/query" - "github.com/zitadel/zitadel/internal/eventstore/v1/spooler" - "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/notification/channels/fs" - "github.com/zitadel/zitadel/internal/notification/channels/log" - "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" - "github.com/zitadel/zitadel/internal/notification/types" - "github.com/zitadel/zitadel/internal/query" - user_repo "github.com/zitadel/zitadel/internal/repository/user" - es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model" - "github.com/zitadel/zitadel/internal/user/repository/view" - "github.com/zitadel/zitadel/internal/user/repository/view/model" -) - -const ( - notificationTable = "notification.notifications" - NotifyUserID = "NOTIFICATION" -) - -type Notification struct { - handler - command *command.Commands - fileSystemPath string - statikDir http.FileSystem - subscription *v1.Subscription - assetsPrefix string - queries *query.Queries - userDataCrypto crypto.EncryptionAlgorithm - smtpPasswordCrypto crypto.EncryptionAlgorithm - smsTokenCrypto crypto.EncryptionAlgorithm - externalPort uint16 - externalSecure bool -} - -func newNotification( - handler handler, - command *command.Commands, - query *query.Queries, - externalPort uint16, - externalSecure bool, - statikDir http.FileSystem, - assetsPrefix, - fileSystemPath string, - userEncryption crypto.EncryptionAlgorithm, - smtpEncryption crypto.EncryptionAlgorithm, - smsEncryption crypto.EncryptionAlgorithm, -) *Notification { - h := &Notification{ - handler: handler, - command: command, - statikDir: statikDir, - assetsPrefix: assetsPrefix, - queries: query, - userDataCrypto: userEncryption, - smtpPasswordCrypto: smtpEncryption, - smsTokenCrypto: smsEncryption, - externalSecure: externalSecure, - externalPort: externalPort, - fileSystemPath: fileSystemPath, - } - - h.subscribe() - - return h -} - -func (k *Notification) subscribe() { - k.subscription = k.es.Subscribe(k.AggregateTypes()...) - go func() { - for event := range k.subscription.Events { - queryv1.ReduceEvent(k, event) - } - }() -} - -func (n *Notification) ViewModel() string { - return notificationTable -} - -func (n *Notification) Subscription() *v1.Subscription { - return n.subscription -} - -func (_ *Notification) AggregateTypes() []models.AggregateType { - return []models.AggregateType{user_repo.AggregateType} -} - -func (n *Notification) CurrentSequence(instanceID string) (uint64, error) { - sequence, err := n.view.GetLatestNotificationSequence(instanceID) - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (n *Notification) EventQuery() (*models.SearchQuery, error) { - sequences, err := n.view.GetLatestNotificationSequences() - if err != nil { - return nil, err - } - query := models.NewSearchQuery() - instances := make([]string, 0) - for _, sequence := range sequences { - for _, instance := range instances { - if sequence.InstanceID == instance { - break - } - } - instances = append(instances, sequence.InstanceID) - query.AddQuery(). - AggregateTypeFilter(n.AggregateTypes()...). - LatestSequenceFilter(sequence.CurrentSequence). - InstanceIDFilter(sequence.InstanceID) - } - return query.AddQuery(). - AggregateTypeFilter(n.AggregateTypes()...). - LatestSequenceFilter(0). - ExcludedInstanceIDsFilter(instances...). - SearchQuery(), nil -} - -func (n *Notification) Reduce(event *models.Event) (err error) { - switch eventstore.EventType(event.Type) { - case user_repo.UserV1InitialCodeAddedType, - user_repo.HumanInitialCodeAddedType: - err = n.handleInitUserCode(event) - case user_repo.UserV1EmailCodeAddedType, - user_repo.HumanEmailCodeAddedType: - err = n.handleEmailVerificationCode(event) - case user_repo.UserV1PhoneCodeAddedType, - user_repo.HumanPhoneCodeAddedType: - err = n.handlePhoneVerificationCode(event) - case user_repo.UserV1PasswordCodeAddedType, - user_repo.HumanPasswordCodeAddedType: - err = n.handlePasswordCode(event) - case user_repo.UserDomainClaimedType: - err = n.handleDomainClaimed(event) - case user_repo.HumanPasswordlessInitCodeRequestedType: - err = n.handlePasswordlessRegistrationLink(event) - } - if err != nil { - return err - } - return n.view.ProcessedNotificationSequence(event) -} - -func (n *Notification) handleInitUserCode(event *models.Event) (err error) { - initCode := new(es_model.InitUserCode) - if err := initCode.SetData(event); err != nil { - return err - } - ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner) - alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, initCode.Expiry, - user_repo.UserV1InitialCodeAddedType, user_repo.UserV1InitialCodeSentType, - user_repo.HumanInitialCodeAddedType, user_repo.HumanInitialCodeSentType) - if err != nil || alreadyHandled { - return err - } - colors, err := n.getLabelPolicy(ctx) - if err != nil { - return err - } - - template, err := n.getMailTemplate(ctx) - if err != nil { - return err - } - - user, err := n.getUserByID(event.AggregateID, event.InstanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - - if user.Sequence < event.Sequence { - if err = n.verifyLatestUser(ctx, user); err != nil { - return err - } - } - - if user.Sequence == 0 { - return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found") - } - - translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.InitCodeMessageType) - if err != nil { - return err - } - - origin, err := n.origin(ctx) - if err != nil { - return err - } - err = types.SendUserInitCode(ctx, string(template.Template), translator, user, initCode, n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin) - if err != nil { - return err - } - return n.command.HumanInitCodeSent(ctx, event.ResourceOwner, event.AggregateID) -} - -func (n *Notification) handlePasswordCode(event *models.Event) (err error) { - pwCode := new(es_model.PasswordCode) - if err := pwCode.SetData(event); err != nil { - return err - } - ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner) - alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, pwCode.Expiry, - user_repo.UserV1PasswordCodeAddedType, user_repo.UserV1PasswordCodeSentType, - user_repo.HumanPasswordCodeAddedType, user_repo.HumanPasswordCodeSentType) - if err != nil || alreadyHandled { - return err - } - colors, err := n.getLabelPolicy(ctx) - if err != nil { - return err - } - - template, err := n.getMailTemplate(ctx) - if err != nil { - return err - } - - user, err := n.getUserByID(event.AggregateID, event.InstanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - - if user.Sequence < event.Sequence { - if err = n.verifyLatestUser(ctx, user); err != nil { - return err - } - } - - if user.Sequence == 0 { - return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found") - } - - translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.PasswordResetMessageType) - if err != nil { - return err - } - - origin, err := n.origin(ctx) - if err != nil { - return err - } - err = types.SendPasswordCode(ctx, string(template.Template), translator, user, pwCode, n.getSMTPConfig, n.getTwilioConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin) - if err != nil { - return err - } - return n.command.PasswordCodeSent(ctx, event.ResourceOwner, event.AggregateID) -} - -func (n *Notification) handleEmailVerificationCode(event *models.Event) (err error) { - emailCode := new(es_model.EmailCode) - if err := emailCode.SetData(event); err != nil { - return err - } - ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner) - alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, emailCode.Expiry, - user_repo.UserV1EmailCodeAddedType, user_repo.UserV1EmailCodeSentType, - user_repo.HumanEmailCodeAddedType, user_repo.HumanEmailCodeSentType) - if err != nil || alreadyHandled { - return nil - } - colors, err := n.getLabelPolicy(ctx) - if err != nil { - return err - } - - template, err := n.getMailTemplate(ctx) - if err != nil { - return err - } - - user, err := n.getUserByID(event.AggregateID, event.InstanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - if user.Sequence < event.Sequence { - if err = n.verifyLatestUser(ctx, user); err != nil { - return err - } - } - - if user.Sequence == 0 { - return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found") - } - - translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.VerifyEmailMessageType) - if err != nil { - return err - } - - origin, err := n.origin(ctx) - if err != nil { - return err - } - err = types.SendEmailVerificationCode(ctx, string(template.Template), translator, user, emailCode, n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin) - if err != nil { - return err - } - return n.command.HumanEmailVerificationCodeSent(ctx, event.ResourceOwner, event.AggregateID) -} - -func (n *Notification) handlePhoneVerificationCode(event *models.Event) (err error) { - phoneCode := new(es_model.PhoneCode) - if err := phoneCode.SetData(event); err != nil { - return err - } - ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner) - alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, phoneCode.Expiry, - user_repo.UserV1PhoneCodeAddedType, user_repo.UserV1PhoneCodeSentType, - user_repo.HumanPhoneCodeAddedType, user_repo.HumanPhoneCodeSentType) - if err != nil || alreadyHandled { - return nil - } - user, err := n.getUserByID(event.AggregateID, event.InstanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - - if user.Sequence < event.Sequence { - if err = n.verifyLatestUser(ctx, user); err != nil { - return err - } - } - - if user.Sequence == 0 { - return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found") - } - - translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.VerifyPhoneMessageType) - if err != nil { - return err - } - err = types.SendPhoneVerificationCode(ctx, translator, user, phoneCode, n.getTwilioConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto) - if err != nil { - return err - } - return n.command.HumanPhoneVerificationCodeSent(ctx, event.ResourceOwner, event.AggregateID) -} - -func (n *Notification) handleDomainClaimed(event *models.Event) (err error) { - ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner) - alreadyHandled, err := n.checkIfAlreadyHandled(ctx, event.AggregateID, event.InstanceID, event.Sequence, user_repo.UserDomainClaimedType, user_repo.UserDomainClaimedSentType) - if err != nil || alreadyHandled { - return nil - } - data := make(map[string]string) - if err := json.Unmarshal(event.Data, &data); err != nil { - logging.Log("HANDLE-Gghq2").WithError(err).Error("could not unmarshal event data") - return errors.ThrowInternal(err, "HANDLE-7hgj3", "could not unmarshal event") - } - user, err := n.getUserByID(event.AggregateID, event.InstanceID) - if err != nil { - return err - } - if user.LastEmail == "" { - return nil - } - colors, err := n.getLabelPolicy(ctx) - if err != nil { - return err - } - - template, err := n.getMailTemplate(ctx) - if err != nil { - return err - } - - translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.DomainClaimedMessageType) - if err != nil { - return err - } - - origin, err := n.origin(ctx) - if err != nil { - return err - } - err = types.SendDomainClaimed(ctx, string(template.Template), translator, user, data["userName"], n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, colors, n.assetsPrefix, origin) - if err != nil { - return err - } - return n.command.UserDomainClaimedSent(ctx, event.ResourceOwner, event.AggregateID) -} - -func (n *Notification) handlePasswordlessRegistrationLink(event *models.Event) (err error) { - addedEvent := new(user_repo.HumanPasswordlessInitCodeRequestedEvent) - if err := json.Unmarshal(event.Data, addedEvent); err != nil { - return err - } - ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner) - events, err := n.getUserEvents(ctx, event.AggregateID, event.InstanceID, event.Sequence) - if err != nil { - return err - } - for _, e := range events { - if eventstore.EventType(e.Type) == user_repo.HumanPasswordlessInitCodeSentType { - sentEvent := new(user_repo.HumanPasswordlessInitCodeSentEvent) - if err := json.Unmarshal(e.Data, sentEvent); err != nil { - return err - } - if sentEvent.ID == addedEvent.ID { - return nil - } - } - } - user, err := n.getUserByID(event.AggregateID, event.InstanceID) - if err != nil { - return err - } - colors, err := n.getLabelPolicy(ctx) - if err != nil { - return err - } - - template, err := n.getMailTemplate(ctx) - if err != nil { - return err - } - - translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.PasswordlessRegistrationMessageType) - if err != nil { - return err - } - - origin, err := n.origin(ctx) - if err != nil { - return err - } - err = types.SendPasswordlessRegistrationLink(ctx, string(template.Template), translator, user, addedEvent, n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin) - if err != nil { - return err - } - return n.command.HumanPasswordlessInitCodeSent(ctx, event.AggregateID, event.ResourceOwner, addedEvent.ID) -} - -func (n *Notification) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event *models.Event, expiry time.Duration, eventTypes ...eventstore.EventType) (bool, error) { - if event.CreationDate.Add(expiry).Before(time.Now().UTC()) { - return true, nil - } - return n.checkIfAlreadyHandled(ctx, event.AggregateID, event.InstanceID, event.Sequence, eventTypes...) -} - -func (n *Notification) checkIfAlreadyHandled(ctx context.Context, userID, instanceID string, sequence uint64, eventTypes ...eventstore.EventType) (bool, error) { - events, err := n.getUserEvents(ctx, userID, instanceID, sequence) - if err != nil { - return false, err - } - for _, event := range events { - for _, eventType := range eventTypes { - if eventstore.EventType(event.Type) == eventType { - return true, nil - } - } - } - return false, nil -} - -func (n *Notification) getUserEvents(ctx context.Context, userID, instanceID string, sequence uint64) ([]*models.Event, error) { - query, err := view.UserByIDQuery(userID, instanceID, sequence) - if err != nil { - return nil, err - } - - return n.es.FilterEvents(ctx, query) -} - -func (n *Notification) OnError(event *models.Event, err error) error { - logging.WithFields("id", event.AggregateID, "sequence", event.Sequence).WithError(err).Warn("something went wrong in notification handler") - return spooler.HandleError(event, err, n.view.GetLatestNotificationFailedEvent, n.view.ProcessedNotificationFailedEvent, n.view.ProcessedNotificationSequence, n.errorCountUntilSkip) -} - -func (n *Notification) OnSuccess() error { - return spooler.HandleSuccess(n.view.UpdateNotificationSpoolerRunTimestamp) -} - -func getSetNotifyContextData(instanceID, orgID string) context.Context { - ctx := authz.WithInstanceID(context.Background(), instanceID) - return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: orgID}) -} - -// Read organization specific colors -func (n *Notification) getLabelPolicy(ctx context.Context) (*query.LabelPolicy, error) { - return n.queries.ActiveLabelPolicyByOrg(ctx, authz.GetCtxData(ctx).OrgID) -} - -// Read organization specific template -func (n *Notification) getMailTemplate(ctx context.Context) (*query.MailTemplate, error) { - return n.queries.MailTemplateByOrg(ctx, authz.GetCtxData(ctx).OrgID) -} - -// Read iam smtp config -func (n *Notification) getSMTPConfig(ctx context.Context) (*smtp.EmailConfig, error) { - config, err := n.queries.SMTPConfigByAggregateID(ctx, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - password, err := crypto.Decrypt(config.Password, n.smtpPasswordCrypto) - if err != nil { - return nil, err - } - return &smtp.EmailConfig{ - From: config.SenderAddress, - FromName: config.SenderName, - Tls: config.TLS, - SMTP: smtp.SMTP{ - Host: config.Host, - User: config.User, - Password: string(password), - }, - }, nil -} - -// Read iam twilio config -func (n *Notification) getTwilioConfig(ctx context.Context) (*twilio.TwilioConfig, error) { - active, err := query.NewSMSProviderStateQuery(domain.SMSConfigStateActive) - if err != nil { - return nil, err - } - config, err := n.queries.SMSProviderConfig(ctx, active) - if err != nil { - return nil, err - } - if config.TwilioConfig == nil { - return nil, errors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound") - } - token, err := crypto.Decrypt(config.TwilioConfig.Token, n.smsTokenCrypto) - if err != nil { - return nil, err - } - return &twilio.TwilioConfig{ - SID: config.TwilioConfig.SID, - Token: string(token), - SenderNumber: config.TwilioConfig.SenderNumber, - }, nil -} - -// Read iam filesystem provider config -func (n *Notification) getFileSystemProvider(ctx context.Context) (*fs.FSConfig, error) { - config, err := n.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeFile) - if err != nil { - return nil, err - } - return &fs.FSConfig{ - Compact: config.Compact, - Path: n.fileSystemPath, - }, nil -} - -// Read iam log provider config -func (n *Notification) getLogProvider(ctx context.Context) (*log.LogConfig, error) { - config, err := n.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeLog) - if err != nil { - return nil, err - } - return &log.LogConfig{ - Compact: config.Compact, - }, nil -} - -func (n *Notification) getTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) { - translator, err := i18n.NewTranslator(n.statikDir, n.queries.GetDefaultLanguage(ctx), "") - if err != nil { - return nil, err - } - - allCustomTexts, err := n.queries.CustomTextListByTemplate(ctx, authz.GetInstance(ctx).InstanceID(), textType) - if err != nil { - return translator, nil - } - customTexts, err := n.queries.CustomTextListByTemplate(ctx, orgID, textType) - if err != nil { - return translator, nil - } - allCustomTexts.CustomTexts = append(allCustomTexts.CustomTexts, customTexts.CustomTexts...) - - for _, text := range allCustomTexts.CustomTexts { - msg := i18n.Message{ - ID: text.Template + "." + text.Key, - Text: text.Text, - } - translator.AddMessages(text.Language, msg) - } - return translator, nil -} - -func (n *Notification) getUserByID(userID, instanceID string) (*model.NotifyUser, error) { - return n.view.NotifyUserByID(userID, instanceID) -} - -func (n *Notification) origin(ctx context.Context) (string, error) { - primary, err := query.NewInstanceDomainPrimarySearchQuery(true) - domains, err := n.queries.SearchInstanceDomains(ctx, &query.InstanceDomainSearchQueries{ - Queries: []query.SearchQuery{primary}, - }) - if err != nil { - return "", err - } - if len(domains.Domains) < 1 { - return "", errors.ThrowInternal(nil, "NOTIF-Ef3r1", "Errors.Notification.NoDomain") - } - return http_utils.BuildHTTP(domains.Domains[0].Domain, n.externalPort, n.externalSecure), nil -} - -func (n *Notification) verifyLatestUser(ctx context.Context, user *model.NotifyUser) error { - events, err := n.getUserEvents(ctx, user.ID, user.InstanceID, user.Sequence) - if err != nil { - return err - } - for _, event := range events { - if err = user.AppendEvent(event); err != nil { - return err - } - } - return nil -} diff --git a/internal/notification/repository/eventsourcing/handler/notify_user.go b/internal/notification/repository/eventsourcing/handler/notify_user.go deleted file mode 100644 index 9ea5081156..0000000000 --- a/internal/notification/repository/eventsourcing/handler/notify_user.go +++ /dev/null @@ -1,278 +0,0 @@ -package handler - -import ( - "context" - - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/api/authz" - caos_errs "github.com/zitadel/zitadel/internal/errors" - "github.com/zitadel/zitadel/internal/eventstore" - v1 "github.com/zitadel/zitadel/internal/eventstore/v1" - es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" - "github.com/zitadel/zitadel/internal/eventstore/v1/query" - es_sdk "github.com/zitadel/zitadel/internal/eventstore/v1/sdk" - "github.com/zitadel/zitadel/internal/eventstore/v1/spooler" - org_model "github.com/zitadel/zitadel/internal/org/model" - org_es_model "github.com/zitadel/zitadel/internal/org/repository/eventsourcing/model" - org_view "github.com/zitadel/zitadel/internal/org/repository/view" - query2 "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/repository/org" - "github.com/zitadel/zitadel/internal/repository/user" - view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" -) - -const ( - userTable = "notification.notify_users" -) - -type NotifyUser struct { - handler - subscription *v1.Subscription - queries *query2.Queries -} - -func newNotifyUser( - handler handler, - queries *query2.Queries, -) *NotifyUser { - h := &NotifyUser{ - handler: handler, - queries: queries, - } - - h.subscribe() - - return h -} - -func (k *NotifyUser) subscribe() { - k.subscription = k.es.Subscribe(k.AggregateTypes()...) - go func() { - for event := range k.subscription.Events { - query.ReduceEvent(k, event) - } - }() -} - -func (p *NotifyUser) ViewModel() string { - return userTable -} - -func (p *NotifyUser) Subscription() *v1.Subscription { - return p.subscription -} - -func (_ *NotifyUser) AggregateTypes() []es_models.AggregateType { - return []es_models.AggregateType{user.AggregateType, org.AggregateType} -} - -func (p *NotifyUser) CurrentSequence(instanceID string) (uint64, error) { - sequence, err := p.view.GetLatestNotifyUserSequence(instanceID) - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (p *NotifyUser) EventQuery() (*es_models.SearchQuery, error) { - sequences, err := p.view.GetLatestNotifyUserSequences() - if err != nil { - return nil, err - } - query := es_models.NewSearchQuery() - instances := make([]string, 0) - for _, sequence := range sequences { - for _, instance := range instances { - if sequence.InstanceID == instance { - break - } - } - instances = append(instances, sequence.InstanceID) - query.AddQuery(). - AggregateTypeFilter(p.AggregateTypes()...). - LatestSequenceFilter(sequence.CurrentSequence). - InstanceIDFilter(sequence.InstanceID) - } - return query.AddQuery(). - AggregateTypeFilter(p.AggregateTypes()...). - LatestSequenceFilter(0). - ExcludedInstanceIDsFilter(instances...). - SearchQuery(), nil -} - -func (u *NotifyUser) Reduce(event *es_models.Event) (err error) { - switch event.AggregateType { - case user.AggregateType: - return u.ProcessUser(event) - case org.AggregateType: - return u.ProcessOrg(event) - default: - return nil - } -} - -func (u *NotifyUser) ProcessUser(event *es_models.Event) (err error) { - notifyUser := new(view_model.NotifyUser) - switch eventstore.EventType(event.Type) { - case user.UserV1AddedType, - user.UserV1RegisteredType, - user.HumanRegisteredType, - user.HumanAddedType, - user.MachineAddedEventType: - err = notifyUser.AppendEvent(event) - if err != nil { - return err - } - err = u.fillLoginNames(notifyUser) - case user.UserV1ProfileChangedType, - user.UserV1EmailChangedType, - user.UserV1EmailVerifiedType, - user.UserV1PhoneChangedType, - user.UserV1PhoneVerifiedType, - user.UserV1PhoneRemovedType, - user.HumanProfileChangedType, - user.HumanEmailChangedType, - user.HumanEmailVerifiedType, - user.HumanPhoneChangedType, - user.HumanPhoneVerifiedType, - user.HumanPhoneRemovedType, - user.MachineChangedEventType: - notifyUser, err = u.view.NotifyUserByID(event.AggregateID, event.InstanceID) - if err != nil { - return err - } - err = notifyUser.AppendEvent(event) - case user.UserDomainClaimedType, - user.UserUserNameChangedType: - notifyUser, err = u.view.NotifyUserByID(event.AggregateID, event.InstanceID) - if err != nil { - return err - } - err = notifyUser.AppendEvent(event) - if err != nil { - return err - } - err = u.fillLoginNames(notifyUser) - case user.UserRemovedType: - return u.view.DeleteNotifyUser(event.AggregateID, event.InstanceID, event) - default: - return u.view.ProcessedNotifyUserSequence(event) - } - if err != nil { - return err - } - return u.view.PutNotifyUser(notifyUser, event) -} - -func (u *NotifyUser) ProcessOrg(event *es_models.Event) (err error) { - switch eventstore.EventType(event.Type) { - case org.OrgDomainVerifiedEventType, - org.OrgDomainRemovedEventType, - org.DomainPolicyAddedEventType, - org.DomainPolicyChangedEventType, - org.DomainPolicyRemovedEventType: - return u.fillLoginNamesOnOrgUsers(event) - case org.OrgDomainPrimarySetEventType: - return u.fillPreferredLoginNamesOnOrgUsers(event) - default: - return u.view.ProcessedNotifyUserSequence(event) - } -} - -func (u *NotifyUser) fillLoginNamesOnOrgUsers(event *es_models.Event) error { - userLoginMustBeDomain, _, domains, err := u.loginNameInformation(context.Background(), event.ResourceOwner, event.InstanceID) - if err != nil { - return err - } - users, err := u.view.NotifyUsersByOrgID(event.AggregateID, event.InstanceID) - if err != nil { - return err - } - for _, user := range users { - user.SetLoginNames(userLoginMustBeDomain, domains) - err := u.view.PutNotifyUser(user, event) - if err != nil { - return err - } - } - return u.view.ProcessedNotifyUserSequence(event) -} - -func (u *NotifyUser) fillPreferredLoginNamesOnOrgUsers(event *es_models.Event) error { - userLoginMustBeDomain, primaryDomain, _, err := u.loginNameInformation(context.Background(), event.ResourceOwner, event.InstanceID) - if err != nil { - return err - } - if !userLoginMustBeDomain { - return nil - } - users, err := u.view.NotifyUsersByOrgID(event.AggregateID, event.InstanceID) - if err != nil { - return err - } - for _, user := range users { - user.PreferredLoginName = user.GenerateLoginName(primaryDomain, userLoginMustBeDomain) - err := u.view.PutNotifyUser(user, event) - if err != nil { - return err - } - } - return nil -} - -func (u *NotifyUser) fillLoginNames(user *view_model.NotifyUser) (err error) { - userLoginMustBeDomain, primaryDomain, domains, err := u.loginNameInformation(context.Background(), user.ResourceOwner, user.InstanceID) - if err != nil { - return err - } - user.SetLoginNames(userLoginMustBeDomain, domains) - user.PreferredLoginName = user.GenerateLoginName(primaryDomain, userLoginMustBeDomain) - return nil -} - -func (p *NotifyUser) OnError(event *es_models.Event, err error) error { - logging.LogWithFields("SPOOL-9spwf", "id", event.AggregateID).WithError(err).Warn("something went wrong in notify user handler") - return spooler.HandleError(event, err, p.view.GetLatestNotifyUserFailedEvent, p.view.ProcessedNotifyUserFailedEvent, p.view.ProcessedNotifyUserSequence, p.errorCountUntilSkip) -} - -func (u *NotifyUser) OnSuccess() error { - return spooler.HandleSuccess(u.view.UpdateNotifyUserSpoolerRunTimestamp) -} - -func (u *NotifyUser) getOrgByID(ctx context.Context, orgID, instanceID string) (*org_model.Org, error) { - query, err := org_view.OrgByIDQuery(orgID, instanceID, 0) - if err != nil { - return nil, err - } - - esOrg := &org_es_model.Org{ - ObjectRoot: es_models.ObjectRoot{ - AggregateID: orgID, - }, - } - err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, query) - if err != nil && !caos_errs.IsNotFound(err) { - return nil, err - } - if esOrg.Sequence == 0 { - return nil, caos_errs.ThrowNotFound(nil, "EVENT-kVLb2", "Errors.Org.NotFound") - } - - return org_es_model.OrgToModel(esOrg), nil -} - -func (u *NotifyUser) loginNameInformation(ctx context.Context, orgID, instanceID string) (userLoginMustBeDomain bool, primaryDomain string, domains []*org_model.OrgDomain, err error) { - org, err := u.getOrgByID(ctx, orgID, instanceID) - if err != nil { - return false, "", nil, err - } - if org.DomainPolicy == nil { - policy, err := u.queries.DefaultDomainPolicy(authz.WithInstanceID(ctx, org.InstanceID)) - if err != nil { - return false, "", nil, err - } - userLoginMustBeDomain = policy.UserLoginMustBeDomain - } - return userLoginMustBeDomain, org.GetPrimaryDomain().Domain, org.Domains, nil -} diff --git a/internal/notification/repository/eventsourcing/repository.go b/internal/notification/repository/eventsourcing/repository.go deleted file mode 100644 index 9bb6065ae1..0000000000 --- a/internal/notification/repository/eventsourcing/repository.go +++ /dev/null @@ -1,56 +0,0 @@ -package eventsourcing - -import ( - "database/sql" - "net/http" - - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/crypto" - v1 "github.com/zitadel/zitadel/internal/eventstore/v1" - es_spol "github.com/zitadel/zitadel/internal/eventstore/v1/spooler" - "github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/spooler" - noti_view "github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/view" - "github.com/zitadel/zitadel/internal/query" -) - -type Config struct { - Spooler spooler.SpoolerConfig -} - -type EsRepository struct { - spooler *es_spol.Spooler -} - -func Start(conf Config, - dir http.FileSystem, - externalPort uint16, - externalSecure bool, - command *command.Commands, - queries *query.Queries, - dbClient *sql.DB, - assetsPrefix, - fileSystemPath string, - userEncryption crypto.EncryptionAlgorithm, - smtpEncryption crypto.EncryptionAlgorithm, - smsEncryption crypto.EncryptionAlgorithm, -) (*EsRepository, error) { - es, err := v1.Start(dbClient) - if err != nil { - return nil, err - } - - view, err := noti_view.StartView(dbClient) - if err != nil { - return nil, err - } - - spool := spooler.StartSpooler(conf.Spooler, es, view, dbClient, command, queries, externalPort, externalSecure, dir, assetsPrefix, fileSystemPath, userEncryption, smtpEncryption, smsEncryption) - - return &EsRepository{ - spool, - }, nil -} - -func (repo *EsRepository) Health() error { - return nil -} diff --git a/internal/notification/repository/eventsourcing/spooler/lock.go b/internal/notification/repository/eventsourcing/spooler/lock.go deleted file mode 100644 index d773a3c399..0000000000 --- a/internal/notification/repository/eventsourcing/spooler/lock.go +++ /dev/null @@ -1,20 +0,0 @@ -package spooler - -import ( - "database/sql" - "time" - - es_locker "github.com/zitadel/zitadel/internal/eventstore/v1/locker" -) - -const ( - lockTable = "notification.locks" -) - -type locker struct { - dbClient *sql.DB -} - -func (l *locker) Renew(lockerID, viewModel, instanceID string, waitTime time.Duration) error { - return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, instanceID, waitTime) -} diff --git a/internal/notification/repository/eventsourcing/spooler/spooler.go b/internal/notification/repository/eventsourcing/spooler/spooler.go deleted file mode 100644 index 685dbe5c44..0000000000 --- a/internal/notification/repository/eventsourcing/spooler/spooler.go +++ /dev/null @@ -1,47 +0,0 @@ -package spooler - -import ( - "database/sql" - "net/http" - - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/crypto" - v1 "github.com/zitadel/zitadel/internal/eventstore/v1" - "github.com/zitadel/zitadel/internal/eventstore/v1/spooler" - "github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/handler" - "github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/view" - "github.com/zitadel/zitadel/internal/query" -) - -type SpoolerConfig struct { - BulkLimit uint64 - FailureCountUntilSkip uint64 - ConcurrentWorkers int - Handlers handler.Configs -} - -func StartSpooler(c SpoolerConfig, - es v1.Eventstore, - view *view.View, - sql *sql.DB, - command *command.Commands, - queries *query.Queries, - externalPort uint16, - externalSecure bool, - dir http.FileSystem, - assetsPrefix, - fileSystemPath string, - userEncryption crypto.EncryptionAlgorithm, - smtpEncryption crypto.EncryptionAlgorithm, - smsEncryption crypto.EncryptionAlgorithm, -) *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, queries, externalPort, externalSecure, dir, assetsPrefix, fileSystemPath, userEncryption, smtpEncryption, smsEncryption), - } - spool := spoolerConfig.New() - spool.Start() - return spool -} diff --git a/internal/notification/repository/eventsourcing/view/error_event.go b/internal/notification/repository/eventsourcing/view/error_event.go deleted file mode 100644 index 73ad6678d8..0000000000 --- a/internal/notification/repository/eventsourcing/view/error_event.go +++ /dev/null @@ -1,17 +0,0 @@ -package view - -import ( - "github.com/zitadel/zitadel/internal/view/repository" -) - -const ( - errTable = "notification.failed_events" -) - -func (v *View) saveFailedEvent(failedEvent *repository.FailedEvent) error { - return repository.SaveFailedEvent(v.Db, errTable, failedEvent) -} - -func (v *View) latestFailedEvent(viewName, instanceID string, sequence uint64) (*repository.FailedEvent, error) { - return repository.LatestFailedEvent(v.Db, errTable, viewName, instanceID, sequence) -} diff --git a/internal/notification/repository/eventsourcing/view/notification.go b/internal/notification/repository/eventsourcing/view/notification.go deleted file mode 100644 index c0810c6b43..0000000000 --- a/internal/notification/repository/eventsourcing/view/notification.go +++ /dev/null @@ -1,34 +0,0 @@ -package view - -import ( - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - "github.com/zitadel/zitadel/internal/view/repository" -) - -const ( - notificationTable = "notification.notifications" -) - -func (v *View) GetLatestNotificationSequence(instanceID string) (*repository.CurrentSequence, error) { - return v.latestSequence(notificationTable, instanceID) -} - -func (v *View) GetLatestNotificationSequences() ([]*repository.CurrentSequence, error) { - return v.latestSequences(notificationTable) -} - -func (v *View) ProcessedNotificationSequence(event *models.Event) error { - return v.saveCurrentSequence(notificationTable, event) -} - -func (v *View) UpdateNotificationSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(notificationTable) -} - -func (v *View) GetLatestNotificationFailedEvent(sequence uint64, instanceID string) (*repository.FailedEvent, error) { - return v.latestFailedEvent(notificationTable, instanceID, sequence) -} - -func (v *View) ProcessedNotificationFailedEvent(failedEvent *repository.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/notification/repository/eventsourcing/view/notify_user.go b/internal/notification/repository/eventsourcing/view/notify_user.go deleted file mode 100644 index d5639264f1..0000000000 --- a/internal/notification/repository/eventsourcing/view/notify_user.go +++ /dev/null @@ -1,61 +0,0 @@ -package view - -import ( - "github.com/zitadel/zitadel/internal/errors" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - "github.com/zitadel/zitadel/internal/user/repository/view" - "github.com/zitadel/zitadel/internal/user/repository/view/model" - "github.com/zitadel/zitadel/internal/view/repository" -) - -const ( - notifyUserTable = "notification.notify_users" -) - -func (v *View) NotifyUserByID(userID, instanceID string) (*model.NotifyUser, error) { - return view.NotifyUserByID(v.Db, notifyUserTable, userID, instanceID) -} - -func (v *View) PutNotifyUser(user *model.NotifyUser, event *models.Event) error { - err := view.PutNotifyUser(v.Db, notifyUserTable, user) - if err != nil { - return err - } - return v.ProcessedNotifyUserSequence(event) -} - -func (v *View) NotifyUsersByOrgID(orgID, instanceID string) ([]*model.NotifyUser, error) { - return view.NotifyUsersByOrgID(v.Db, notifyUserTable, orgID, instanceID) -} - -func (v *View) DeleteNotifyUser(userID, instanceID string, event *models.Event) error { - err := view.DeleteNotifyUser(v.Db, notifyUserTable, userID, instanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedNotifyUserSequence(event) -} - -func (v *View) GetLatestNotifyUserSequence(instanceID string) (*repository.CurrentSequence, error) { - return v.latestSequence(notifyUserTable, instanceID) -} - -func (v *View) GetLatestNotifyUserSequences() ([]*repository.CurrentSequence, error) { - return v.latestSequences(notifyUserTable) -} - -func (v *View) ProcessedNotifyUserSequence(event *models.Event) error { - return v.saveCurrentSequence(notifyUserTable, event) -} - -func (v *View) UpdateNotifyUserSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(notifyUserTable) -} - -func (v *View) GetLatestNotifyUserFailedEvent(sequence uint64, instanceID string) (*repository.FailedEvent, error) { - return v.latestFailedEvent(notifyUserTable, instanceID, sequence) -} - -func (v *View) ProcessedNotifyUserFailedEvent(failedEvent *repository.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/notification/repository/eventsourcing/view/sequence.go b/internal/notification/repository/eventsourcing/view/sequence.go deleted file mode 100644 index 8c7307ef72..0000000000 --- a/internal/notification/repository/eventsourcing/view/sequence.go +++ /dev/null @@ -1,38 +0,0 @@ -package view - -import ( - "time" - - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - "github.com/zitadel/zitadel/internal/view/repository" -) - -const ( - sequencesTable = "notification.current_sequences" -) - -func (v *View) saveCurrentSequence(viewName string, event *models.Event) error { - return repository.SaveCurrentSequence(v.Db, sequencesTable, viewName, event.InstanceID, event.Sequence, event.CreationDate) -} - -func (v *View) latestSequence(viewName, instanceID string) (*repository.CurrentSequence, error) { - return repository.LatestSequence(v.Db, sequencesTable, viewName, instanceID) -} - -func (v *View) latestSequences(viewName string) ([]*repository.CurrentSequence, error) { - return repository.LatestSequences(v.Db, sequencesTable, viewName) -} - -func (v *View) updateSpoolerRunSequence(viewName string) error { - currentSequences, err := repository.LatestSequences(v.Db, sequencesTable, viewName) - if err != nil { - return err - } - for _, currentSequence := range currentSequences { - if currentSequence.ViewName == "" { - currentSequence.ViewName = viewName - } - currentSequence.LastSuccessfulSpoolerRun = time.Now() - } - return repository.UpdateCurrentSequences(v.Db, sequencesTable, currentSequences) -} diff --git a/internal/notification/repository/eventsourcing/view/view.go b/internal/notification/repository/eventsourcing/view/view.go deleted file mode 100644 index 4b8c52392d..0000000000 --- a/internal/notification/repository/eventsourcing/view/view.go +++ /dev/null @@ -1,25 +0,0 @@ -package view - -import ( - "database/sql" - - "github.com/jinzhu/gorm" -) - -type View struct { - Db *gorm.DB -} - -func StartView(sqlClient *sql.DB) (*View, error) { - gorm, err := gorm.Open("postgres", sqlClient) - if err != nil { - return nil, err - } - return &View{ - Db: gorm, - }, nil -} - -func (v *View) Health() (err error) { - return v.Db.DB().Ping() -} diff --git a/internal/notification/repository/repository.go b/internal/notification/repository/repository.go deleted file mode 100644 index f99114e05f..0000000000 --- a/internal/notification/repository/repository.go +++ /dev/null @@ -1,5 +0,0 @@ -package repository - -type Repository interface { - Health() error -} diff --git a/internal/notification/templates/templateData.go b/internal/notification/templates/templateData.go index 75b4433923..29a74f3328 100644 --- a/internal/notification/templates/templateData.go +++ b/internal/notification/templates/templateData.go @@ -21,7 +21,7 @@ type TemplateData struct { Subject string Greeting string Text string - Href string + URL string ButtonText string PrimaryColor string BackgroundColor string diff --git a/internal/notification/types/domain_claimed.go b/internal/notification/types/domain_claimed.go index 4ebad6708c..cc6159454b 100644 --- a/internal/notification/types/domain_claimed.go +++ b/internal/notification/types/domain_claimed.go @@ -1,38 +1,17 @@ package types import ( - "context" "strings" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/notification/channels/fs" - "github.com/zitadel/zitadel/internal/notification/channels/log" - "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/templates" "github.com/zitadel/zitadel/internal/query" - view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" ) -type DomainClaimedData struct { - templates.TemplateData - URL string -} - -func SendDomainClaimed(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, username string, emailConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), colors *query.LabelPolicy, assetsPrefix string, origin string) error { +func (notify Notify) SendDomainClaimed(user *query.NotifyUser, origin, username string) error { url := login.LoginLink(origin, user.ResourceOwner) - var args = mapNotifyUserToArgs(user) + args := make(map[string]interface{}) args["TempUsername"] = username args["Domain"] = strings.Split(user.LastEmail, "@")[1] - - domainClaimedData := &DomainClaimedData{ - TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.DomainClaimedMessageType, user.PreferredLanguage, colors), - URL: url, - } - template, err := templates.GetParsedTemplate(mailhtml, domainClaimedData) - if err != nil { - return err - } - return generateEmail(ctx, user, domainClaimedData.Subject, template, emailConfig, getFileSystemProvider, getLogProvider, true) + return notify(url, args, domain.DomainClaimedMessageType, true) } diff --git a/internal/notification/types/email_verification_code.go b/internal/notification/types/email_verification_code.go index 9170e41b78..5f4ab071fb 100644 --- a/internal/notification/types/email_verification_code.go +++ b/internal/notification/types/email_verification_code.go @@ -1,43 +1,14 @@ package types import ( - "context" - "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/notification/channels/fs" - "github.com/zitadel/zitadel/internal/notification/channels/log" - "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/templates" "github.com/zitadel/zitadel/internal/query" - es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model" - view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" ) -type EmailVerificationCodeData struct { - templates.TemplateData - URL string -} - -func SendEmailVerificationCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.EmailCode, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string, origin string) error { - codeString, err := crypto.DecryptString(code.Code, alg) - if err != nil { - return err - } - url := login.MailVerificationLink(origin, user.ID, codeString, user.ResourceOwner) - var args = mapNotifyUserToArgs(user) - args["Code"] = codeString - - emailCodeData := &EmailVerificationCodeData{ - TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.VerifyEmailMessageType, user.PreferredLanguage, colors), - URL: url, - } - - template, err := templates.GetParsedTemplate(mailhtml, emailCodeData) - if err != nil { - return err - } - return generateEmail(ctx, user, emailCodeData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true) +func (notify Notify) SendEmailVerificationCode(user *query.NotifyUser, origin, code string) error { + url := login.MailVerificationLink(origin, user.ID, code, user.ResourceOwner) + args := make(map[string]interface{}) + args["Code"] = code + return notify(url, args, domain.VerifyEmailMessageType, true) } diff --git a/internal/notification/types/init_code.go b/internal/notification/types/init_code.go index e4b6095e28..01399ef29d 100644 --- a/internal/notification/types/init_code.go +++ b/internal/notification/types/init_code.go @@ -1,49 +1,14 @@ package types import ( - "context" - "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/notification/channels/fs" - "github.com/zitadel/zitadel/internal/notification/channels/log" - "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/templates" "github.com/zitadel/zitadel/internal/query" - es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model" - view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" ) -type InitCodeEmailData struct { - templates.TemplateData - URL string -} - -type UrlData struct { - UserID string - Code string - PasswordSet bool - OrgID string -} - -func SendUserInitCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.InitUserCode, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix, origin string) error { - codeString, err := crypto.DecryptString(code.Code, alg) - if err != nil { - return err - } - url := login.InitUserLink(origin, user.ID, codeString, user.ResourceOwner, user.PasswordSet) - var args = mapNotifyUserToArgs(user) - args["Code"] = codeString - - initCodeData := &InitCodeEmailData{ - TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.InitCodeMessageType, user.PreferredLanguage, colors), - URL: url, - } - template, err := templates.GetParsedTemplate(mailhtml, initCodeData) - if err != nil { - return err - } - return generateEmail(ctx, user, initCodeData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true) +func (notify Notify) SendUserInitCode(user *query.NotifyUser, origin, code string) error { + url := login.InitUserLink(origin, user.ID, code, user.ResourceOwner, user.PasswordSet) + args := make(map[string]interface{}) + args["Code"] = code + return notify(url, args, domain.InitCodeMessageType, true) } diff --git a/internal/notification/types/notification.go b/internal/notification/types/notification.go new file mode 100644 index 0000000000..117e272add --- /dev/null +++ b/internal/notification/types/notification.go @@ -0,0 +1,73 @@ +package types + +import ( + "context" + + "github.com/zitadel/zitadel/internal/i18n" + "github.com/zitadel/zitadel/internal/notification/channels/fs" + "github.com/zitadel/zitadel/internal/notification/channels/log" + "github.com/zitadel/zitadel/internal/notification/channels/smtp" + "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/templates" + "github.com/zitadel/zitadel/internal/query" +) + +type Notify func( + url string, + args map[string]interface{}, + messageType string, + allowUnverifiedNotificationChannel bool, +) error + +func SendEmail( + ctx context.Context, + mailhtml string, + translator *i18n.Translator, + user *query.NotifyUser, + emailConfig func(ctx context.Context) (*smtp.EmailConfig, error), + getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), + getLogProvider func(ctx context.Context) (*log.LogConfig, error), + colors *query.LabelPolicy, + assetsPrefix string, +) Notify { + return func( + url string, + args map[string]interface{}, + messageType string, + allowUnverifiedNotificationChannel bool, + ) error { + args = mapNotifyUserToArgs(user, args) + data := GetTemplateData(translator, args, assetsPrefix, url, messageType, user.PreferredLanguage.String(), colors) + template, err := templates.GetParsedTemplate(mailhtml, data) + if err != nil { + return err + } + return generateEmail(ctx, user, data.Subject, template, emailConfig, getFileSystemProvider, getLogProvider, allowUnverifiedNotificationChannel) + } +} + +func SendSMSTwilio( + ctx context.Context, + translator *i18n.Translator, + user *query.NotifyUser, + twilioConfig func(ctx context.Context) (*twilio.TwilioConfig, error), + getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), + getLogProvider func(ctx context.Context) (*log.LogConfig, error), + colors *query.LabelPolicy, + assetsPrefix string, +) Notify { + return func( + url string, + args map[string]interface{}, + messageType string, + allowUnverifiedNotificationChannel bool, + ) error { + args = mapNotifyUserToArgs(user, args) + data := GetTemplateData(translator, args, assetsPrefix, url, messageType, user.PreferredLanguage.String(), colors) + return generateSms(ctx, user, data.Text, twilioConfig, getFileSystemProvider, getLogProvider, allowUnverifiedNotificationChannel) + } +} + +func externalLink(origin string) string { + return origin + "/ui/login" +} diff --git a/internal/notification/types/password_code.go b/internal/notification/types/password_code.go index 9baa41123f..e9b5df529f 100644 --- a/internal/notification/types/password_code.go +++ b/internal/notification/types/password_code.go @@ -1,51 +1,14 @@ package types import ( - "context" - "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/notification/channels/fs" - "github.com/zitadel/zitadel/internal/notification/channels/log" - "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" - "github.com/zitadel/zitadel/internal/notification/templates" "github.com/zitadel/zitadel/internal/query" - es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model" - view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" ) -type PasswordCodeData struct { - templates.TemplateData - FirstName string - LastName string - URL string -} - -func SendPasswordCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.PasswordCode, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getTwilioConfig func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string, origin string) error { - codeString, err := crypto.DecryptString(code.Code, alg) - if err != nil { - return err - } - url := login.InitPasswordLink(origin, user.ID, codeString, user.ResourceOwner) - var args = mapNotifyUserToArgs(user) - args["Code"] = codeString - - passwordResetData := &PasswordCodeData{ - TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.PasswordResetMessageType, user.PreferredLanguage, colors), - FirstName: user.FirstName, - LastName: user.LastName, - URL: url, - } - template, err := templates.GetParsedTemplate(mailhtml, passwordResetData) - if err != nil { - return err - } - if code.NotificationType == int32(domain.NotificationTypeSms) { - return generateSms(ctx, user, passwordResetData.Text, getTwilioConfig, getFileSystemProvider, getLogProvider, false) - } - return generateEmail(ctx, user, passwordResetData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true) - +func (notify Notify) SendPasswordCode(user *query.NotifyUser, origin, code string) error { + url := login.InitPasswordLink(origin, user.ID, code, user.ResourceOwner) + args := make(map[string]interface{}) + args["Code"] = code + return notify(url, args, domain.PasswordResetMessageType, true) } diff --git a/internal/notification/types/passwordless_registration_link.go b/internal/notification/types/passwordless_registration_link.go index 88c2381eaf..8c6f6894ba 100644 --- a/internal/notification/types/passwordless_registration_link.go +++ b/internal/notification/types/passwordless_registration_link.go @@ -1,42 +1,12 @@ package types import ( - "context" - "github.com/zitadel/zitadel/internal/api/ui/login" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/notification/channels/fs" - "github.com/zitadel/zitadel/internal/notification/channels/log" - "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/templates" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/repository/user" - view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" ) -type PasswordlessRegistrationLinkData struct { - templates.TemplateData - URL string -} - -func SendPasswordlessRegistrationLink(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *user.HumanPasswordlessInitCodeRequestedEvent, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string, origin string) error { - codeString, err := crypto.DecryptString(code.Code, alg) - if err != nil { - return err - } - url := domain.PasswordlessInitCodeLink(origin+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, code.ID, codeString) - var args = mapNotifyUserToArgs(user) - - emailCodeData := &PasswordlessRegistrationLinkData{ - TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.PasswordlessRegistrationMessageType, user.PreferredLanguage, colors), - URL: url, - } - - template, err := templates.GetParsedTemplate(mailhtml, emailCodeData) - if err != nil { - return err - } - return generateEmail(ctx, user, emailCodeData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true) +func (notify Notify) SendPasswordlessRegistrationLink(user *query.NotifyUser, origin, code, codeID string) error { + url := domain.PasswordlessInitCodeLink(origin+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code) + return notify(url, nil, domain.PasswordlessRegistrationMessageType, true) } diff --git a/internal/notification/types/phone_verification_code.go b/internal/notification/types/phone_verification_code.go index e37d39a1ea..88eb744c4b 100644 --- a/internal/notification/types/phone_verification_code.go +++ b/internal/notification/types/phone_verification_code.go @@ -1,38 +1,12 @@ package types import ( - "context" - "fmt" - - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/notification/channels/fs" - "github.com/zitadel/zitadel/internal/notification/channels/log" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" - "github.com/zitadel/zitadel/internal/notification/templates" - es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model" - view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" + "github.com/zitadel/zitadel/internal/query" ) -type PhoneVerificationCodeData struct { - UserID string -} - -func SendPhoneVerificationCode(ctx context.Context, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.PhoneCode, getTwilioConfig func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm) error { - codeString, err := crypto.DecryptString(code.Code, alg) - if err != nil { - return err - } - var args = mapNotifyUserToArgs(user) - args["Code"] = codeString - - text := translator.Localize(fmt.Sprintf("%s.%s", domain.VerifyPhoneMessageType, domain.MessageText), args, user.PreferredLanguage) - - codeData := &PhoneVerificationCodeData{UserID: user.ID} - template, err := templates.ParseTemplateText(text, codeData) - if err != nil { - return err - } - return generateSms(ctx, user, template, getTwilioConfig, getFileSystemProvider, getLogProvider, true) +func (notify Notify) SendPhoneVerificationCode(user *query.NotifyUser, origin, code string) error { + args := make(map[string]interface{}) + args["Code"] = code + return notify("", args, domain.VerifyPhoneMessageType, true) } diff --git a/internal/notification/types/templateData.go b/internal/notification/types/templateData.go index ae464f69e1..465e17555c 100644 --- a/internal/notification/types/templateData.go +++ b/internal/notification/types/templateData.go @@ -11,7 +11,7 @@ import ( func GetTemplateData(translator *i18n.Translator, translateArgs map[string]interface{}, assetsPrefix, href, msgType, lang string, policy *query.LabelPolicy) templates.TemplateData { templateData := templates.TemplateData{ - Href: href, + URL: href, PrimaryColor: templates.DefaultPrimaryColor, BackgroundColor: templates.DefaultBackgroundColor, FontColor: templates.DefaultFontColor, diff --git a/internal/notification/types/user_email.go b/internal/notification/types/user_email.go index 7b4cf37ef9..afcc302835 100644 --- a/internal/notification/types/user_email.go +++ b/internal/notification/types/user_email.go @@ -10,11 +10,10 @@ import ( "github.com/zitadel/zitadel/internal/notification/channels/smtp" "github.com/zitadel/zitadel/internal/notification/messages" "github.com/zitadel/zitadel/internal/notification/senders" - - view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" + "github.com/zitadel/zitadel/internal/query" ) -func generateEmail(ctx context.Context, user *view_model.NotifyUser, subject, content string, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastEmail bool) error { +func generateEmail(ctx context.Context, user *query.NotifyUser, subject, content string, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastEmail bool) error { content = html.UnescapeString(content) message := &messages.Email{ Recipients: []string{user.VerifiedEmail}, @@ -36,20 +35,22 @@ func generateEmail(ctx context.Context, user *view_model.NotifyUser, subject, co return channelChain.HandleMessage(message) } -func mapNotifyUserToArgs(user *view_model.NotifyUser) map[string]interface{} { - return map[string]interface{}{ - "UserName": user.UserName, - "FirstName": user.FirstName, - "LastName": user.LastName, - "NickName": user.NickName, - "DisplayName": user.DisplayName, - "LastEmail": user.LastEmail, - "VerifiedEmail": user.VerifiedEmail, - "LastPhone": user.LastPhone, - "VerifiedPhone": user.VerifiedPhone, - "PreferredLoginName": user.PreferredLoginName, - "LoginNames": user.LoginNames, - "ChangeDate": user.ChangeDate, - "CreationDate": user.CreationDate, +func mapNotifyUserToArgs(user *query.NotifyUser, args map[string]interface{}) map[string]interface{} { + if args == nil { + args = make(map[string]interface{}) } + args["UserName"] = user.Username + args["FirstName"] = user.FirstName + args["LastName"] = user.LastName + args["NickName"] = user.NickName + args["DisplayName"] = user.DisplayName + args["LastEmail"] = user.LastEmail + args["VerifiedEmail"] = user.VerifiedEmail + args["LastPhone"] = user.LastPhone + args["VerifiedPhone"] = user.VerifiedPhone + args["PreferredLoginName"] = user.PreferredLoginName + args["LoginNames"] = user.LoginNames + args["ChangeDate"] = user.ChangeDate + args["CreationDate"] = user.CreationDate + return args } diff --git a/internal/notification/types/user_phone.go b/internal/notification/types/user_phone.go index 5d7663edb1..ea9462ff97 100644 --- a/internal/notification/types/user_phone.go +++ b/internal/notification/types/user_phone.go @@ -3,20 +3,22 @@ package types import ( "context" + "github.com/zitadel/logging" + caos_errors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/notification/channels/fs" "github.com/zitadel/zitadel/internal/notification/channels/log" "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/notification/messages" "github.com/zitadel/zitadel/internal/notification/senders" - view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" + "github.com/zitadel/zitadel/internal/query" ) -func generateSms(ctx context.Context, user *view_model.NotifyUser, content string, getTwilioProvider func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastPhone bool) error { +func generateSms(ctx context.Context, user *query.NotifyUser, content string, getTwilioProvider func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastPhone bool) error { number := "" - twilio, err := getTwilioProvider(ctx) + twilioConfig, err := getTwilioProvider(ctx) if err == nil { - number = twilio.SenderNumber + number = twilioConfig.SenderNumber } message := &messages.SMS{ SenderPhoneNumber: number, @@ -27,7 +29,8 @@ func generateSms(ctx context.Context, user *view_model.NotifyUser, content strin message.RecipientPhoneNumber = user.LastPhone } - channelChain, err := senders.SMSChannels(ctx, twilio, getFileSystemProvider, getLogProvider) + channelChain, err := senders.SMSChannels(ctx, twilioConfig, getFileSystemProvider, getLogProvider) + logging.OnError(err).Error("could not create sms channel") if channelChain.Len() == 0 { return caos_errors.ThrowPreconditionFailed(nil, "PHONE-w8nfow", "Errors.Notification.Channels.NotPresent") diff --git a/internal/query/iam_member_test.go b/internal/query/iam_member_test.go index 0d740e5846..50896e73d0 100644 --- a/internal/query/iam_member_test.go +++ b/internal/query/iam_member_test.go @@ -20,18 +20,18 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names.login_name" + - ", projections.users_humans.email" + - ", projections.users_humans.first_name" + - ", projections.users_humans.last_name" + - ", projections.users_humans.display_name" + - ", projections.users_machines.name" + - ", projections.users_humans.avatar_key" + + ", projections.users2_humans.email" + + ", projections.users2_humans.first_name" + + ", projections.users2_humans.last_name" + + ", projections.users2_humans.display_name" + + ", projections.users2_machines.name" + + ", projections.users2_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.instance_members as members " + - "LEFT JOIN projections.users_humans " + - "ON members.user_id = projections.users_humans.user_id " + - "LEFT JOIN projections.users_machines " + - "ON members.user_id = projections.users_machines.user_id " + + "LEFT JOIN projections.users2_humans " + + "ON members.user_id = projections.users2_humans.user_id " + + "LEFT JOIN projections.users2_machines " + + "ON members.user_id = projections.users2_machines.user_id " + "LEFT JOIN projections.login_names " + "ON members.user_id = projections.login_names.user_id " + "WHERE projections.login_names.is_primary = $1") diff --git a/internal/query/org_member_test.go b/internal/query/org_member_test.go index e176774c31..4b89ad59df 100644 --- a/internal/query/org_member_test.go +++ b/internal/query/org_member_test.go @@ -20,18 +20,18 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names.login_name" + - ", projections.users_humans.email" + - ", projections.users_humans.first_name" + - ", projections.users_humans.last_name" + - ", projections.users_humans.display_name" + - ", projections.users_machines.name" + - ", projections.users_humans.avatar_key" + + ", projections.users2_humans.email" + + ", projections.users2_humans.first_name" + + ", projections.users2_humans.last_name" + + ", projections.users2_humans.display_name" + + ", projections.users2_machines.name" + + ", projections.users2_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.org_members as members " + - "LEFT JOIN projections.users_humans " + - "ON members.user_id = projections.users_humans.user_id " + - "LEFT JOIN projections.users_machines " + - "ON members.user_id = projections.users_machines.user_id " + + "LEFT JOIN projections.users2_humans " + + "ON members.user_id = projections.users2_humans.user_id " + + "LEFT JOIN projections.users2_machines " + + "ON members.user_id = projections.users2_machines.user_id " + "LEFT JOIN projections.login_names " + "ON members.user_id = projections.login_names.user_id " + "WHERE projections.login_names.is_primary = $1") diff --git a/internal/query/project_grant_member_test.go b/internal/query/project_grant_member_test.go index 58069c2908..d449dd926e 100644 --- a/internal/query/project_grant_member_test.go +++ b/internal/query/project_grant_member_test.go @@ -20,18 +20,18 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names.login_name" + - ", projections.users_humans.email" + - ", projections.users_humans.first_name" + - ", projections.users_humans.last_name" + - ", projections.users_humans.display_name" + - ", projections.users_machines.name" + - ", projections.users_humans.avatar_key" + + ", projections.users2_humans.email" + + ", projections.users2_humans.first_name" + + ", projections.users2_humans.last_name" + + ", projections.users2_humans.display_name" + + ", projections.users2_machines.name" + + ", projections.users2_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.project_grant_members as members " + - "LEFT JOIN projections.users_humans " + - "ON members.user_id = projections.users_humans.user_id " + - "LEFT JOIN projections.users_machines " + - "ON members.user_id = projections.users_machines.user_id " + + "LEFT JOIN projections.users2_humans " + + "ON members.user_id = projections.users2_humans.user_id " + + "LEFT JOIN projections.users2_machines " + + "ON members.user_id = projections.users2_machines.user_id " + "LEFT JOIN projections.login_names " + "ON members.user_id = projections.login_names.user_id " + "LEFT JOIN projections.project_grants " + diff --git a/internal/query/project_member_test.go b/internal/query/project_member_test.go index 222153a100..dbef1b8283 100644 --- a/internal/query/project_member_test.go +++ b/internal/query/project_member_test.go @@ -20,18 +20,18 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names.login_name" + - ", projections.users_humans.email" + - ", projections.users_humans.first_name" + - ", projections.users_humans.last_name" + - ", projections.users_humans.display_name" + - ", projections.users_machines.name" + - ", projections.users_humans.avatar_key" + + ", projections.users2_humans.email" + + ", projections.users2_humans.first_name" + + ", projections.users2_humans.last_name" + + ", projections.users2_humans.display_name" + + ", projections.users2_machines.name" + + ", projections.users2_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.project_members as members " + - "LEFT JOIN projections.users_humans " + - "ON members.user_id = projections.users_humans.user_id " + - "LEFT JOIN projections.users_machines " + - "ON members.user_id = projections.users_machines.user_id " + + "LEFT JOIN projections.users2_humans " + + "ON members.user_id = projections.users2_humans.user_id " + + "LEFT JOIN projections.users2_machines " + + "ON members.user_id = projections.users2_machines.user_id " + "LEFT JOIN projections.login_names " + "ON members.user_id = projections.login_names.user_id " + "WHERE projections.login_names.is_primary = $1") diff --git a/internal/query/projection/label_policy.go b/internal/query/projection/label_policy.go index f15a76c6ee..3bd2f61d8f 100644 --- a/internal/query/projection/label_policy.go +++ b/internal/query/projection/label_policy.go @@ -358,6 +358,32 @@ func (p *labelPolicyProjection) reduceActivated(event eventstore.Event) (*handle handler.NewCol(LabelPolicyDarkLogoURLCol, nil), handler.NewCol(LabelPolicyDarkIconURLCol, nil), }, + []handler.Column{ + handler.NewCol(LabelPolicyChangeDateCol, nil), + handler.NewCol(LabelPolicySequenceCol, nil), + handler.NewCol(LabelPolicyStateCol, nil), + handler.NewCol(LabelPolicyCreationDateCol, nil), + handler.NewCol(LabelPolicyResourceOwnerCol, nil), + handler.NewCol(LabelPolicyInstanceIDCol, nil), + handler.NewCol(LabelPolicyIDCol, nil), + handler.NewCol(LabelPolicyIsDefaultCol, nil), + handler.NewCol(LabelPolicyHideLoginNameSuffixCol, nil), + handler.NewCol(LabelPolicyFontURLCol, nil), + handler.NewCol(LabelPolicyWatermarkDisabledCol, nil), + handler.NewCol(LabelPolicyShouldErrorPopupCol, nil), + handler.NewCol(LabelPolicyLightPrimaryColorCol, nil), + handler.NewCol(LabelPolicyLightWarnColorCol, nil), + handler.NewCol(LabelPolicyLightBackgroundColorCol, nil), + handler.NewCol(LabelPolicyLightFontColorCol, nil), + handler.NewCol(LabelPolicyLightLogoURLCol, nil), + handler.NewCol(LabelPolicyLightIconURLCol, nil), + handler.NewCol(LabelPolicyDarkPrimaryColorCol, nil), + handler.NewCol(LabelPolicyDarkWarnColorCol, nil), + handler.NewCol(LabelPolicyDarkBackgroundColorCol, nil), + handler.NewCol(LabelPolicyDarkFontColorCol, nil), + handler.NewCol(LabelPolicyDarkLogoURLCol, nil), + handler.NewCol(LabelPolicyDarkIconURLCol, nil), + }, []handler.Condition{ handler.NewCond(LabelPolicyIDCol, event.Aggregate().ID), handler.NewCond(LabelPolicyStateCol, domain.LabelPolicyStatePreview), diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 4e377b8628..1995831dfa 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -18,6 +18,7 @@ const ( ) var ( + projectionConfig crdb.StatementHandlerConfig OrgProjection *orgProjection ActionProjection *actionProjection FlowProjection *flowProjection @@ -58,10 +59,11 @@ var ( OIDCSettingsProjection *oidcSettingsProjection DebugNotificationProviderProjection *debugNotificationProviderProjection KeyProjection *keyProjection + NotificationsProjection interface{} ) func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, config Config, keyEncryptionAlgorithm crypto.EncryptionAlgorithm) error { - projectionConfig := crdb.StatementHandlerConfig{ + projectionConfig = crdb.StatementHandlerConfig{ ProjectionHandlerConfig: handler.ProjectionHandlerConfig{ HandlerConfig: handler.HandlerConfig{ Eventstore: es, @@ -120,6 +122,11 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co return nil } +func ApplyCustomConfig(customConfig CustomConfig) crdb.StatementHandlerConfig { + return applyCustomConfig(projectionConfig, customConfig) + +} + func applyCustomConfig(config crdb.StatementHandlerConfig, customConfig CustomConfig) crdb.StatementHandlerConfig { if customConfig.BulkLimit != nil { config.BulkLimit = *customConfig.BulkLimit diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index 800a21d063..fc762761d6 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -17,9 +17,10 @@ type userProjection struct { } const ( - UserTable = "projections.users" + UserTable = "projections.users2" UserHumanTable = UserTable + "_" + UserHumanSuffix UserMachineTable = UserTable + "_" + UserMachineSuffix + UserNotifyTable = UserTable + "_" + UserNotifySuffix UserIDCol = "id" UserCreationDateCol = "creation_date" @@ -58,6 +59,16 @@ const ( MachineUserInstanceIDCol = "instance_id" MachineNameCol = "name" MachineDescriptionCol = "description" + + // notify + UserNotifySuffix = "notifications" + NotifyUserIDCol = "user_id" + NotifyInstanceIDCol = "instance_id" + NotifyLastEmailCol = "last_email" + NotifyVerifiedEmailCol = "verified_email" + NotifyLastPhoneCol = "last_phone" + NotifyVerifiedPhoneCol = "verified_phone" + NotifyPasswordSetCol = "password_set" ) func newUserProjection(ctx context.Context, config crdb.StatementHandlerConfig) *userProjection { @@ -110,6 +121,19 @@ func newUserProjection(ctx context.Context, config crdb.StatementHandlerConfig) UserMachineSuffix, crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_machine_ref_user")), ), + crdb.NewSuffixedTable([]*crdb.Column{ + crdb.NewColumn(NotifyUserIDCol, crdb.ColumnTypeText), + crdb.NewColumn(NotifyInstanceIDCol, crdb.ColumnTypeText), + crdb.NewColumn(NotifyLastEmailCol, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(NotifyVerifiedEmailCol, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(NotifyLastPhoneCol, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(NotifyVerifiedPhoneCol, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(NotifyPasswordSetCol, crdb.ColumnTypeBool, crdb.Default(false)), + }, + crdb.NewPrimaryKey(NotifyUserIDCol, NotifyInstanceIDCol), + UserNotifySuffix, + crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_notify_ref_user")), + ), ) p.StatementHandler = crdb.NewStatementHandler(ctx, config) return p @@ -240,6 +264,10 @@ func (p *userProjection) reducers() []handler.AggregateReducer { Event: user.MachineChangedEventType, Reduce: p.reduceMachineChanged, }, + { + Event: user.HumanPasswordChangedType, + Reduce: p.reduceHumanPasswordChanged, + }, }, }, } @@ -280,6 +308,16 @@ func (p *userProjection) reduceHumanAdded(event eventstore.Event) (*handler.Stat }, crdb.WithTableSuffix(UserHumanSuffix), ), + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(NotifyUserIDCol, e.Aggregate().ID), + handler.NewCol(NotifyInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCol(NotifyLastEmailCol, e.EmailAddress), + handler.NewCol(NotifyLastPhoneCol, &sql.NullString{String: e.PhoneNumber, Valid: e.PhoneNumber != ""}), + handler.NewCol(NotifyPasswordSetCol, e.Secret != nil), + }, + crdb.WithTableSuffix(UserNotifySuffix), + ), ), nil } @@ -318,6 +356,16 @@ func (p *userProjection) reduceHumanRegistered(event eventstore.Event) (*handler }, crdb.WithTableSuffix(UserHumanSuffix), ), + crdb.AddCreateStatement( + []handler.Column{ + handler.NewCol(NotifyUserIDCol, e.Aggregate().ID), + handler.NewCol(NotifyInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCol(NotifyLastEmailCol, e.EmailAddress), + handler.NewCol(NotifyLastPhoneCol, &sql.NullString{String: e.PhoneNumber, Valid: e.PhoneNumber != ""}), + handler.NewCol(NotifyPasswordSetCol, e.Secret != nil), + }, + crdb.WithTableSuffix(UserNotifySuffix), + ), ), nil } @@ -552,6 +600,16 @@ func (p *userProjection) reduceHumanPhoneChanged(event eventstore.Event) (*handl }, crdb.WithTableSuffix(UserHumanSuffix), ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(NotifyLastPhoneCol, &sql.NullString{String: e.PhoneNumber, Valid: e.PhoneNumber != ""}), + }, + []handler.Condition{ + handler.NewCond(NotifyUserIDCol, e.Aggregate().ID), + handler.NewCond(NotifyInstanceIDCol, e.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(UserNotifySuffix), + ), ), nil } @@ -584,6 +642,17 @@ func (p *userProjection) reduceHumanPhoneRemoved(event eventstore.Event) (*handl }, crdb.WithTableSuffix(UserHumanSuffix), ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(NotifyLastPhoneCol, nil), + handler.NewCol(NotifyVerifiedPhoneCol, nil), + }, + []handler.Condition{ + handler.NewCond(NotifyUserIDCol, e.Aggregate().ID), + handler.NewCond(NotifyInstanceIDCol, e.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(UserNotifySuffix), + ), ), nil } @@ -615,6 +684,23 @@ func (p *userProjection) reduceHumanPhoneVerified(event eventstore.Event) (*hand }, crdb.WithTableSuffix(UserHumanSuffix), ), + crdb.AddCopyStatement( + []handler.Column{ + handler.NewCol(NotifyUserIDCol, nil), + handler.NewCol(NotifyInstanceIDCol, nil), + handler.NewCol(NotifyLastPhoneCol, nil), + }, + []handler.Column{ + handler.NewCol(NotifyUserIDCol, nil), + handler.NewCol(NotifyInstanceIDCol, nil), + handler.NewCol(NotifyVerifiedPhoneCol, nil), + }, + []handler.Condition{ + handler.NewCond(NotifyUserIDCol, e.Aggregate().ID), + handler.NewCond(NotifyInstanceIDCol, e.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(UserNotifySuffix), + ), ), nil } @@ -647,6 +733,16 @@ func (p *userProjection) reduceHumanEmailChanged(event eventstore.Event) (*handl }, crdb.WithTableSuffix(UserHumanSuffix), ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(NotifyLastEmailCol, &sql.NullString{String: e.EmailAddress, Valid: e.EmailAddress != ""}), + }, + []handler.Condition{ + handler.NewCond(NotifyUserIDCol, e.Aggregate().ID), + handler.NewCond(NotifyInstanceIDCol, e.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(UserNotifySuffix), + ), ), nil } @@ -678,6 +774,23 @@ func (p *userProjection) reduceHumanEmailVerified(event eventstore.Event) (*hand }, crdb.WithTableSuffix(UserHumanSuffix), ), + crdb.AddCopyStatement( + []handler.Column{ + handler.NewCol(NotifyUserIDCol, nil), + handler.NewCol(NotifyInstanceIDCol, nil), + handler.NewCol(NotifyLastEmailCol, nil), + }, + []handler.Column{ + handler.NewCol(NotifyUserIDCol, nil), + handler.NewCol(NotifyInstanceIDCol, nil), + handler.NewCol(NotifyVerifiedEmailCol, nil), + }, + []handler.Condition{ + handler.NewCond(NotifyUserIDCol, e.Aggregate().ID), + handler.NewCond(NotifyInstanceIDCol, e.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(UserNotifySuffix), + ), ), nil } @@ -743,6 +856,25 @@ func (p *userProjection) reduceHumanAvatarRemoved(event eventstore.Event) (*hand ), nil } +func (p *userProjection) reduceHumanPasswordChanged(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.HumanPasswordChangedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-jqXUY", "reduce.wrong.event.type %s", user.HumanPasswordChangedType) + } + + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(NotifyPasswordSetCol, true), + }, + []handler.Condition{ + handler.NewCond(NotifyUserIDCol, e.Aggregate().ID), + handler.NewCond(NotifyInstanceIDCol, e.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(UserNotifySuffix), + ), nil +} + func (p *userProjection) reduceMachineAdded(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*user.MachineAddedEvent) if !ok { diff --git a/internal/query/projection/user_test.go b/internal/query/projection/user_test.go index f65da31458..085192161b 100644 --- a/internal/query/projection/user_test.go +++ b/internal/query/projection/user_test.go @@ -50,7 +50,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users2 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -64,7 +64,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users2_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -78,6 +78,16 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{String: "+41 00 000 00 00", Valid: true}, }, }, + { + expectedStmt: "INSERT INTO projections.users2_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + "email@zitadel.com", + &sql.NullString{String: "+41 00 000 00 00", Valid: true}, + false, + }, + }, }, }, }, @@ -110,7 +120,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users2 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -124,7 +134,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users2_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -138,6 +148,16 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{String: "+41 00 000 00 00", Valid: true}, }, }, + { + expectedStmt: "INSERT INTO projections.users2_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + "email@zitadel.com", + &sql.NullString{String: "+41 00 000 00 00", Valid: true}, + false, + }, + }, }, }, }, @@ -165,7 +185,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users2 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -179,7 +199,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users2_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -193,6 +213,16 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{}, }, }, + { + expectedStmt: "INSERT INTO projections.users2_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + "email@zitadel.com", + &sql.NullString{String: "", Valid: false}, + false, + }, + }, }, }, }, @@ -225,7 +255,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users2 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -239,7 +269,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users2_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -253,6 +283,16 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{String: "+41 00 000 00 00", Valid: true}, }, }, + { + expectedStmt: "INSERT INTO projections.users2_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + "email@zitadel.com", + &sql.NullString{String: "+41 00 000 00 00", Valid: true}, + false, + }, + }, }, }, }, @@ -285,7 +325,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users2 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -299,7 +339,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users2_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -313,6 +353,16 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{String: "+41 00 000 00 00", Valid: true}, }, }, + { + expectedStmt: "INSERT INTO projections.users2_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + "email@zitadel.com", + &sql.NullString{String: "+41 00 000 00 00", Valid: true}, + false, + }, + }, }, }, }, @@ -340,7 +390,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users2 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -354,7 +404,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users2_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -368,6 +418,16 @@ func TestUserProjection_reduces(t *testing.T) { &sql.NullString{}, }, }, + { + expectedStmt: "INSERT INTO projections.users2_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + "email@zitadel.com", + &sql.NullString{String: "", Valid: false}, + false, + }, + }, }, }, }, @@ -390,7 +450,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (state) = ($1) WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2 SET (state) = ($1) WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateInitial, "agg-id", @@ -419,7 +479,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (state) = ($1) WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2 SET (state) = ($1) WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateInitial, "agg-id", @@ -448,7 +508,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (state) = ($1) WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2 SET (state) = ($1) WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateActive, "agg-id", @@ -477,7 +537,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (state) = ($1) WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2 SET (state) = ($1) WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateActive, "agg-id", @@ -506,7 +566,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users2 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateLocked, @@ -537,7 +597,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users2 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateActive, @@ -568,7 +628,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users2 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateInactive, @@ -599,7 +659,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users2 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateActive, @@ -630,7 +690,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.users WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.users2 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -660,7 +720,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users2 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, "username", @@ -698,7 +758,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -707,7 +767,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.users2_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "first-name", "last-name", @@ -748,7 +808,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -757,7 +817,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.users2_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "first-name", "last-name", @@ -793,7 +853,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -802,7 +862,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "+41 00 000 00 00", false, @@ -810,6 +870,14 @@ func TestUserProjection_reduces(t *testing.T) { "instance-id", }, }, + { + expectedStmt: "UPDATE projections.users2_notifications SET (last_phone) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + &sql.NullString{String: "+41 00 000 00 00", Valid: true}, + "agg-id", + "instance-id", + }, + }, }, }, }, @@ -834,7 +902,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -843,7 +911,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "+41 00 000 00 00", false, @@ -851,6 +919,14 @@ func TestUserProjection_reduces(t *testing.T) { "instance-id", }, }, + { + expectedStmt: "UPDATE projections.users2_notifications SET (last_phone) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + &sql.NullString{String: "+41 00 000 00 00", Valid: true}, + "agg-id", + "instance-id", + }, + }, }, }, }, @@ -873,7 +949,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -882,7 +958,16 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + nil, + nil, + "agg-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.users2_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -912,7 +997,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -921,7 +1006,16 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + nil, + nil, + "agg-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.users2_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -951,7 +1045,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -960,13 +1054,20 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (is_phone_verified) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2_humans SET (is_phone_verified) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", "instance-id", }, }, + { + expectedStmt: "UPSERT INTO projections.users2_notifications (user_id, instance_id, verified_phone) SELECT user_id, instance_id, last_phone FROM projections.users2_notifications AS copy_table WHERE copy_table.user_id = $1 AND copy_table.instance_id = $2", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + }, + }, }, }, }, @@ -989,7 +1090,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -998,13 +1099,20 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (is_phone_verified) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2_humans SET (is_phone_verified) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", "instance-id", }, }, + { + expectedStmt: "UPSERT INTO projections.users2_notifications (user_id, instance_id, verified_phone) SELECT user_id, instance_id, last_phone FROM projections.users2_notifications AS copy_table WHERE copy_table.user_id = $1 AND copy_table.instance_id = $2", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + }, + }, }, }, }, @@ -1029,7 +1137,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1038,7 +1146,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "email@zitadel.com", false, @@ -1046,6 +1154,14 @@ func TestUserProjection_reduces(t *testing.T) { "instance-id", }, }, + { + expectedStmt: "UPDATE projections.users2_notifications SET (last_email) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + &sql.NullString{String: "email@zitadel.com", Valid: true}, + "agg-id", + "instance-id", + }, + }, }, }, }, @@ -1070,7 +1186,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1079,7 +1195,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "email@zitadel.com", false, @@ -1087,6 +1203,14 @@ func TestUserProjection_reduces(t *testing.T) { "instance-id", }, }, + { + expectedStmt: "UPDATE projections.users2_notifications SET (last_email) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + &sql.NullString{String: "email@zitadel.com", Valid: true}, + "agg-id", + "instance-id", + }, + }, }, }, }, @@ -1109,7 +1233,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1118,13 +1242,20 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (is_email_verified) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2_humans SET (is_email_verified) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", "instance-id", }, }, + { + expectedStmt: "UPSERT INTO projections.users2_notifications (user_id, instance_id, verified_email) SELECT user_id, instance_id, last_email FROM projections.users2_notifications AS copy_table WHERE copy_table.user_id = $1 AND copy_table.instance_id = $2", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + }, + }, }, }, }, @@ -1147,7 +1278,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1156,13 +1287,20 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (is_email_verified) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2_humans SET (is_email_verified) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", "instance-id", }, }, + { + expectedStmt: "UPSERT INTO projections.users2_notifications (user_id, instance_id, verified_email) SELECT user_id, instance_id, last_email FROM projections.users2_notifications AS copy_table WHERE copy_table.user_id = $1 AND copy_table.instance_id = $2", + expectedArgs: []interface{}{ + "agg-id", + "instance-id", + }, + }, }, }, }, @@ -1187,7 +1325,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1196,7 +1334,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (avatar_key) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2_humans SET (avatar_key) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "users/agg-id/avatar", "agg-id", @@ -1225,7 +1363,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1234,7 +1372,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_humans SET (avatar_key) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2_humans SET (avatar_key) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ nil, "agg-id", @@ -1266,7 +1404,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users2 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -1280,7 +1418,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", + expectedStmt: "INSERT INTO projections.users2_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1314,7 +1452,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users2 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -1328,7 +1466,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", + expectedStmt: "INSERT INTO projections.users2_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1361,7 +1499,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1370,7 +1508,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_machines SET (name, description) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2_machines SET (name, description) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "machine-name", "description", @@ -1402,7 +1540,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1411,7 +1549,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_machines SET (name) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2_machines SET (name) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "machine-name", "agg-id", @@ -1442,7 +1580,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1451,7 +1589,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users_machines SET (description) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users2_machines SET (description) = ($1) WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "description", "agg-id", diff --git a/internal/query/user.go b/internal/query/user.go index 1fe93b61ca..1d928f38fe 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -11,9 +11,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query/projection" ) @@ -92,6 +90,31 @@ type Machine struct { Description string } +type NotifyUser struct { + ID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + State domain.UserState + Type domain.UserType + Username string + LoginNames []string + PreferredLoginName string + FirstName string + LastName string + NickName string + DisplayName string + AvatarKey string + PreferredLanguage language.Tag + Gender domain.Gender + LastEmail string + VerifiedEmail string + LastPhone string + VerifiedPhone string + PasswordSet bool +} + type UserSearchQueries struct { SearchRequest Queries []SearchQuery @@ -237,6 +260,38 @@ var ( } ) +var ( + notifyTable = table{ + name: projection.UserNotifyTable, + } + NotifyUserIDCol = Column{ + name: projection.NotifyUserIDCol, + table: notifyTable, + } + NotifyEmailCol = Column{ + name: projection.NotifyLastEmailCol, + table: notifyTable, + isOrderByLower: true, + } + NotifyVerifiedEmailCol = Column{ + name: projection.NotifyVerifiedEmailCol, + table: notifyTable, + isOrderByLower: true, + } + NotifyPhoneCol = Column{ + name: projection.NotifyLastPhoneCol, + table: notifyTable, + } + NotifyVerifiedPhoneCol = Column{ + name: projection.NotifyVerifiedPhoneCol, + table: notifyTable, + } + NotifyPasswordSetCol = Column{ + name: projection.NotifyPasswordSetCol, + table: notifyTable, + } +) + func (q *Queries) GetUserByID(ctx context.Context, shouldTriggered bool, userID string, queries ...SearchQuery) (*User, error) { if shouldTriggered { projection.UserProjection.TriggerBulk(ctx) @@ -327,6 +382,28 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S return scan(row) } +func (q *Queries) GeNotifyUser(ctx context.Context, shouldTriggered bool, userID string, queries ...SearchQuery) (*NotifyUser, error) { + if shouldTriggered { + projection.UserProjection.TriggerBulk(ctx) + } + + instanceID := authz.GetInstance(ctx).InstanceID() + query, scan := prepareNotifyUserQuery(instanceID) + for _, q := range queries { + query = q.toQuery(query) + } + stmt, args, err := query.Where(sq.Eq{ + UserIDCol.identifier(): userID, + UserInstanceIDCol.identifier(): instanceID, + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries) (*Users, error) { query, scan := prepareUsersQuery() stmt, args, err := queries.toQuery(query). @@ -748,6 +825,143 @@ func preparePhoneQuery() (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) { } } +func prepareNotifyUserQuery(instanceID string) (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { + loginNamesQuery, loginNamesArgs, err := sq.Select( + userLoginNamesUserIDCol.identifier(), + "ARRAY_AGG("+userLoginNamesNameCol.identifier()+") as "+userLoginNamesListCol.name). + From(userLoginNamesTable.identifier()). + GroupBy(userLoginNamesUserIDCol.identifier()). + Where(sq.Eq{ + userLoginNamesInstanceIDCol.identifier(): instanceID, + }).ToSql() + if err != nil { + return sq.SelectBuilder{}, nil + } + preferredLoginNameQuery, preferredLoginNameArgs, err := sq.Select( + userPreferredLoginNameUserIDCol.identifier(), + userPreferredLoginNameCol.identifier()). + From(userPreferredLoginNameTable.identifier()). + Where(sq.Eq{ + userPreferredLoginNameIsPrimaryCol.identifier(): true, + userPreferredLoginNameInstanceIDCol.identifier(): instanceID, + }).ToSql() + if err != nil { + return sq.SelectBuilder{}, nil + } + return sq.Select( + UserIDCol.identifier(), + UserCreationDateCol.identifier(), + UserChangeDateCol.identifier(), + UserResourceOwnerCol.identifier(), + UserSequenceCol.identifier(), + UserStateCol.identifier(), + UserTypeCol.identifier(), + UserUsernameCol.identifier(), + userLoginNamesListCol.identifier(), + userPreferredLoginNameCol.identifier(), + HumanUserIDCol.identifier(), + HumanFirstNameCol.identifier(), + HumanLastNameCol.identifier(), + HumanNickNameCol.identifier(), + HumanDisplayNameCol.identifier(), + HumanPreferredLanguageCol.identifier(), + HumanGenderCol.identifier(), + HumanAvatarURLCol.identifier(), + NotifyUserIDCol.identifier(), + NotifyEmailCol.identifier(), + NotifyVerifiedEmailCol.identifier(), + NotifyPhoneCol.identifier(), + NotifyVerifiedPhoneCol.identifier(), + NotifyPasswordSetCol.identifier(), + ). + From(userTable.identifier()). + LeftJoin(join(HumanUserIDCol, UserIDCol)). + LeftJoin(join(NotifyUserIDCol, UserIDCol)). + LeftJoin("("+loginNamesQuery+") as "+userLoginNamesTable.alias+" on "+userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier(), loginNamesArgs...). + LeftJoin("("+preferredLoginNameQuery+") as "+userPreferredLoginNameTable.alias+" on "+userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier(), preferredLoginNameArgs...). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*NotifyUser, error) { + u := new(NotifyUser) + loginNames := pq.StringArray{} + preferredLoginName := sql.NullString{} + + humanID := sql.NullString{} + firstName := sql.NullString{} + lastName := sql.NullString{} + nickName := sql.NullString{} + displayName := sql.NullString{} + preferredLanguage := sql.NullString{} + gender := sql.NullInt32{} + avatarKey := sql.NullString{} + + notifyUserID := sql.NullString{} + notifyEmail := sql.NullString{} + notifyVerifiedEmail := sql.NullString{} + notifyPhone := sql.NullString{} + notifyVerifiedPhone := sql.NullString{} + notifyPasswordSet := sql.NullBool{} + + err := row.Scan( + &u.ID, + &u.CreationDate, + &u.ChangeDate, + &u.ResourceOwner, + &u.Sequence, + &u.State, + &u.Type, + &u.Username, + &loginNames, + &preferredLoginName, + &humanID, + &firstName, + &lastName, + &nickName, + &displayName, + &preferredLanguage, + &gender, + &avatarKey, + ¬ifyUserID, + ¬ifyEmail, + ¬ifyVerifiedEmail, + ¬ifyPhone, + ¬ifyVerifiedPhone, + ¬ifyPasswordSet, + ) + + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-Dgqd2", "Errors.User.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-Dbwsg", "Errors.Internal") + } + + if !notifyUserID.Valid { + return nil, errors.ThrowPreconditionFailed(nil, "QUERY-Sfw3f", "Errors.User.NotFound") + } + + u.LoginNames = loginNames + if preferredLoginName.Valid { + u.PreferredLoginName = preferredLoginName.String + } + if humanID.Valid { + u.FirstName = firstName.String + u.LastName = lastName.String + u.NickName = nickName.String + u.DisplayName = displayName.String + u.AvatarKey = avatarKey.String + u.PreferredLanguage = language.Make(preferredLanguage.String) + u.Gender = domain.Gender(gender.Int32) + } + u.LastEmail = notifyEmail.String + u.VerifiedEmail = notifyVerifiedEmail.String + u.LastPhone = notifyPhone.String + u.VerifiedPhone = notifyVerifiedPhone.String + u.PasswordSet = notifyPasswordSet.Bool + + return u, nil + } +} + func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) { return sq.Select( UserIDCol.identifier(), diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index 3c978c1618..9b3f24bb36 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -24,14 +24,14 @@ var ( ", projections.user_grants.roles" + ", projections.user_grants.state" + ", projections.user_grants.user_id" + - ", projections.users.username" + - ", projections.users.type" + - ", projections.users.resource_owner" + - ", projections.users_humans.first_name" + - ", projections.users_humans.last_name" + - ", projections.users_humans.email" + - ", projections.users_humans.display_name" + - ", projections.users_humans.avatar_key" + + ", projections.users2.username" + + ", projections.users2.type" + + ", projections.users2.resource_owner" + + ", projections.users2_humans.first_name" + + ", projections.users2_humans.last_name" + + ", projections.users2_humans.email" + + ", projections.users2_humans.display_name" + + ", projections.users2_humans.avatar_key" + ", projections.login_names.login_name" + ", projections.user_grants.resource_owner" + ", projections.orgs.name" + @@ -39,8 +39,8 @@ var ( ", projections.user_grants.project_id" + ", projections.projects.name" + " FROM projections.user_grants" + - " LEFT JOIN projections.users ON projections.user_grants.user_id = projections.users.id" + - " LEFT JOIN projections.users_humans ON projections.user_grants.user_id = projections.users_humans.user_id" + + " LEFT JOIN projections.users2 ON projections.user_grants.user_id = projections.users2.id" + + " LEFT JOIN projections.users2_humans ON projections.user_grants.user_id = projections.users2_humans.user_id" + " LEFT JOIN projections.orgs ON projections.user_grants.resource_owner = projections.orgs.id" + " LEFT JOIN projections.projects ON projections.user_grants.project_id = projections.projects.id" + " LEFT JOIN projections.login_names ON projections.user_grants.user_id = projections.login_names.user_id" + @@ -78,14 +78,14 @@ var ( ", projections.user_grants.roles" + ", projections.user_grants.state" + ", projections.user_grants.user_id" + - ", projections.users.username" + - ", projections.users.type" + - ", projections.users.resource_owner" + - ", projections.users_humans.first_name" + - ", projections.users_humans.last_name" + - ", projections.users_humans.email" + - ", projections.users_humans.display_name" + - ", projections.users_humans.avatar_key" + + ", projections.users2.username" + + ", projections.users2.type" + + ", projections.users2.resource_owner" + + ", projections.users2_humans.first_name" + + ", projections.users2_humans.last_name" + + ", projections.users2_humans.email" + + ", projections.users2_humans.display_name" + + ", projections.users2_humans.avatar_key" + ", projections.login_names.login_name" + ", projections.user_grants.resource_owner" + ", projections.orgs.name" + @@ -94,8 +94,8 @@ var ( ", projections.projects.name" + ", COUNT(*) OVER ()" + " FROM projections.user_grants" + - " LEFT JOIN projections.users ON projections.user_grants.user_id = projections.users.id" + - " LEFT JOIN projections.users_humans ON projections.user_grants.user_id = projections.users_humans.user_id" + + " LEFT JOIN projections.users2 ON projections.user_grants.user_id = projections.users2.id" + + " LEFT JOIN projections.users2_humans ON projections.user_grants.user_id = projections.users2_humans.user_id" + " LEFT JOIN projections.orgs ON projections.user_grants.resource_owner = projections.orgs.id" + " LEFT JOIN projections.projects ON projections.user_grants.project_id = projections.projects.id" + " LEFT JOIN projections.login_names ON projections.user_grants.user_id = projections.login_names.user_id" + diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 82958053d6..98a496511e 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -17,43 +17,43 @@ import ( ) var ( - userQuery = `SELECT projections.users.id,` + - ` projections.users.creation_date,` + - ` projections.users.change_date,` + - ` projections.users.resource_owner,` + - ` projections.users.sequence,` + - ` projections.users.state,` + - ` projections.users.type,` + - ` projections.users.username,` + + userQuery = `SELECT projections.users2.id,` + + ` projections.users2.creation_date,` + + ` projections.users2.change_date,` + + ` projections.users2.resource_owner,` + + ` projections.users2.sequence,` + + ` projections.users2.state,` + + ` projections.users2.type,` + + ` projections.users2.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users_humans.user_id,` + - ` projections.users_humans.first_name,` + - ` projections.users_humans.last_name,` + - ` projections.users_humans.nick_name,` + - ` projections.users_humans.display_name,` + - ` projections.users_humans.preferred_language,` + - ` projections.users_humans.gender,` + - ` projections.users_humans.avatar_key,` + - ` projections.users_humans.email,` + - ` projections.users_humans.is_email_verified,` + - ` projections.users_humans.phone,` + - ` projections.users_humans.is_phone_verified,` + - ` projections.users_machines.user_id,` + - ` projections.users_machines.name,` + - ` projections.users_machines.description` + - ` FROM projections.users` + - ` LEFT JOIN projections.users_humans ON projections.users.id = projections.users_humans.user_id` + - ` LEFT JOIN projections.users_machines ON projections.users.id = projections.users_machines.user_id` + + ` projections.users2_humans.user_id,` + + ` projections.users2_humans.first_name,` + + ` projections.users2_humans.last_name,` + + ` projections.users2_humans.nick_name,` + + ` projections.users2_humans.display_name,` + + ` projections.users2_humans.preferred_language,` + + ` projections.users2_humans.gender,` + + ` projections.users2_humans.avatar_key,` + + ` projections.users2_humans.email,` + + ` projections.users2_humans.is_email_verified,` + + ` projections.users2_humans.phone,` + + ` projections.users2_humans.is_phone_verified,` + + ` projections.users2_machines.user_id,` + + ` projections.users2_machines.name,` + + ` projections.users2_machines.description` + + ` FROM projections.users2` + + ` LEFT JOIN projections.users2_humans ON projections.users2.id = projections.users2_humans.user_id` + + ` LEFT JOIN projections.users2_machines ON projections.users2.id = projections.users2_machines.user_id` + ` LEFT JOIN` + ` (SELECT login_names.user_id, ARRAY_AGG(login_names.login_name) as loginnames` + ` FROM projections.login_names as login_names` + ` WHERE login_names.instance_id = $1` + ` GROUP BY login_names.user_id) as login_names` + - ` on login_names.user_id = projections.users.id` + + ` on login_names.user_id = projections.users2.id` + ` LEFT JOIN` + ` (SELECT preferred_login_name.user_id, preferred_login_name.login_name FROM projections.login_names as preferred_login_name WHERE preferred_login_name.instance_id = $2 AND preferred_login_name.is_primary = $3) as preferred_login_name` + - ` on preferred_login_name.user_id = projections.users.id` + ` on preferred_login_name.user_id = projections.users2.id` userCols = []string{ "id", "creation_date", @@ -83,21 +83,21 @@ var ( "name", "description", } - profileQuery = `SELECT projections.users.id,` + - ` projections.users.creation_date,` + - ` projections.users.change_date,` + - ` projections.users.resource_owner,` + - ` projections.users.sequence,` + - ` projections.users_humans.user_id,` + - ` projections.users_humans.first_name,` + - ` projections.users_humans.last_name,` + - ` projections.users_humans.nick_name,` + - ` projections.users_humans.display_name,` + - ` projections.users_humans.preferred_language,` + - ` projections.users_humans.gender,` + - ` projections.users_humans.avatar_key` + - ` FROM projections.users` + - ` LEFT JOIN projections.users_humans ON projections.users.id = projections.users_humans.user_id` + profileQuery = `SELECT projections.users2.id,` + + ` projections.users2.creation_date,` + + ` projections.users2.change_date,` + + ` projections.users2.resource_owner,` + + ` projections.users2.sequence,` + + ` projections.users2_humans.user_id,` + + ` projections.users2_humans.first_name,` + + ` projections.users2_humans.last_name,` + + ` projections.users2_humans.nick_name,` + + ` projections.users2_humans.display_name,` + + ` projections.users2_humans.preferred_language,` + + ` projections.users2_humans.gender,` + + ` projections.users2_humans.avatar_key` + + ` FROM projections.users2` + + ` LEFT JOIN projections.users2_humans ON projections.users2.id = projections.users2_humans.user_id` profileCols = []string{ "id", "creation_date", @@ -113,16 +113,16 @@ var ( "gender", "avatar_key", } - emailQuery = `SELECT projections.users.id,` + - ` projections.users.creation_date,` + - ` projections.users.change_date,` + - ` projections.users.resource_owner,` + - ` projections.users.sequence,` + - ` projections.users_humans.user_id,` + - ` projections.users_humans.email,` + - ` projections.users_humans.is_email_verified` + - ` FROM projections.users` + - ` LEFT JOIN projections.users_humans ON projections.users.id = projections.users_humans.user_id` + emailQuery = `SELECT projections.users2.id,` + + ` projections.users2.creation_date,` + + ` projections.users2.change_date,` + + ` projections.users2.resource_owner,` + + ` projections.users2.sequence,` + + ` projections.users2_humans.user_id,` + + ` projections.users2_humans.email,` + + ` projections.users2_humans.is_email_verified` + + ` FROM projections.users2` + + ` LEFT JOIN projections.users2_humans ON projections.users2.id = projections.users2_humans.user_id` emailCols = []string{ "id", "creation_date", @@ -133,16 +133,16 @@ var ( "email", "is_email_verified", } - phoneQuery = `SELECT projections.users.id,` + - ` projections.users.creation_date,` + - ` projections.users.change_date,` + - ` projections.users.resource_owner,` + - ` projections.users.sequence,` + - ` projections.users_humans.user_id,` + - ` projections.users_humans.phone,` + - ` projections.users_humans.is_phone_verified` + - ` FROM projections.users` + - ` LEFT JOIN projections.users_humans ON projections.users.id = projections.users_humans.user_id` + phoneQuery = `SELECT projections.users2.id,` + + ` projections.users2.creation_date,` + + ` projections.users2.change_date,` + + ` projections.users2.resource_owner,` + + ` projections.users2.sequence,` + + ` projections.users2_humans.user_id,` + + ` projections.users2_humans.phone,` + + ` projections.users2_humans.is_phone_verified` + + ` FROM projections.users2` + + ` LEFT JOIN projections.users2_humans ON projections.users2.id = projections.users2_humans.user_id` phoneCols = []string{ "id", "creation_date", @@ -153,15 +153,14 @@ var ( "phone", "is_phone_verified", } - - userUniqueQuery = `SELECT projections.users.id,` + - ` projections.users.state,` + - ` projections.users.username,` + - ` projections.users_humans.user_id,` + - ` projections.users_humans.email,` + - ` projections.users_humans.is_email_verified` + - ` FROM projections.users` + - ` LEFT JOIN projections.users_humans ON projections.users.id = projections.users_humans.user_id` + userUniqueQuery = `SELECT projections.users2.id,` + + ` projections.users2.state,` + + ` projections.users2.username,` + + ` projections.users2_humans.user_id,` + + ` projections.users2_humans.email,` + + ` projections.users2_humans.is_email_verified` + + ` FROM projections.users2` + + ` LEFT JOIN projections.users2_humans ON projections.users2.id = projections.users2_humans.user_id` userUniqueCols = []string{ "id", "state", @@ -170,43 +169,107 @@ var ( "email", "is_email_verified", } - usersQuery = `SELECT projections.users.id,` + - ` projections.users.creation_date,` + - ` projections.users.change_date,` + - ` projections.users.resource_owner,` + - ` projections.users.sequence,` + - ` projections.users.state,` + - ` projections.users.type,` + - ` projections.users.username,` + + notifyUserQuery = `SELECT projections.users2.id,` + + ` projections.users2.creation_date,` + + ` projections.users2.change_date,` + + ` projections.users2.resource_owner,` + + ` projections.users2.sequence,` + + ` projections.users2.state,` + + ` projections.users2.type,` + + ` projections.users2.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users_humans.user_id,` + - ` projections.users_humans.first_name,` + - ` projections.users_humans.last_name,` + - ` projections.users_humans.nick_name,` + - ` projections.users_humans.display_name,` + - ` projections.users_humans.preferred_language,` + - ` projections.users_humans.gender,` + - ` projections.users_humans.avatar_key,` + - ` projections.users_humans.email,` + - ` projections.users_humans.is_email_verified,` + - ` projections.users_humans.phone,` + - ` projections.users_humans.is_phone_verified,` + - ` projections.users_machines.user_id,` + - ` projections.users_machines.name,` + - ` projections.users_machines.description,` + + ` projections.users2_humans.user_id,` + + ` projections.users2_humans.first_name,` + + ` projections.users2_humans.last_name,` + + ` projections.users2_humans.nick_name,` + + ` projections.users2_humans.display_name,` + + ` projections.users2_humans.preferred_language,` + + ` projections.users2_humans.gender,` + + ` projections.users2_humans.avatar_key,` + + ` projections.users2_notifications.user_id,` + + ` projections.users2_notifications.last_email,` + + ` projections.users2_notifications.verified_email,` + + ` projections.users2_notifications.last_phone,` + + ` projections.users2_notifications.verified_phone,` + + ` projections.users2_notifications.password_set` + + ` FROM projections.users2` + + ` LEFT JOIN projections.users2_humans ON projections.users2.id = projections.users2_humans.user_id` + + ` LEFT JOIN projections.users2_notifications ON projections.users2.id = projections.users2_notifications.user_id` + + ` LEFT JOIN` + + ` (SELECT login_names.user_id, ARRAY_AGG(login_names.login_name) as loginnames` + + ` FROM projections.login_names as login_names` + + ` WHERE login_names.instance_id = $1` + + ` GROUP BY login_names.user_id) as login_names` + + ` on login_names.user_id = projections.users2.id` + + ` LEFT JOIN` + + ` (SELECT preferred_login_name.user_id, preferred_login_name.login_name FROM projections.login_names as preferred_login_name WHERE preferred_login_name.instance_id = $2 AND preferred_login_name.is_primary = $3) as preferred_login_name` + + ` on preferred_login_name.user_id = projections.users2.id` + notifyUserCols = []string{ + "id", + "creation_date", + "change_date", + "resource_owner", + "sequence", + "state", + "type", + "username", + "loginnames", + "login_name", + //human + "user_id", + "first_name", + "last_name", + "nick_name", + "display_name", + "preferred_language", + "gender", + "avatar_key", + //machine + "user_id", + "last_email", + "verified_email", + "last_phone", + "verified_phone", + "password_set", + } + usersQuery = `SELECT projections.users2.id,` + + ` projections.users2.creation_date,` + + ` projections.users2.change_date,` + + ` projections.users2.resource_owner,` + + ` projections.users2.sequence,` + + ` projections.users2.state,` + + ` projections.users2.type,` + + ` projections.users2.username,` + + ` login_names.loginnames,` + + ` preferred_login_name.login_name,` + + ` projections.users2_humans.user_id,` + + ` projections.users2_humans.first_name,` + + ` projections.users2_humans.last_name,` + + ` projections.users2_humans.nick_name,` + + ` projections.users2_humans.display_name,` + + ` projections.users2_humans.preferred_language,` + + ` projections.users2_humans.gender,` + + ` projections.users2_humans.avatar_key,` + + ` projections.users2_humans.email,` + + ` projections.users2_humans.is_email_verified,` + + ` projections.users2_humans.phone,` + + ` projections.users2_humans.is_phone_verified,` + + ` projections.users2_machines.user_id,` + + ` projections.users2_machines.name,` + + ` projections.users2_machines.description,` + ` COUNT(*) OVER ()` + - ` FROM projections.users` + - ` LEFT JOIN projections.users_humans ON projections.users.id = projections.users_humans.user_id` + - ` LEFT JOIN projections.users_machines ON projections.users.id = projections.users_machines.user_id` + + ` FROM projections.users2` + + ` LEFT JOIN projections.users2_humans ON projections.users2.id = projections.users2_humans.user_id` + + ` LEFT JOIN projections.users2_machines ON projections.users2.id = projections.users2_machines.user_id` + ` LEFT JOIN` + ` (SELECT login_names.user_id, ARRAY_AGG(login_names.login_name) as loginnames` + ` FROM projections.login_names as login_names` + ` GROUP BY login_names.user_id) as login_names` + - ` on login_names.user_id = projections.users.id` + + ` on login_names.user_id = projections.users2.id` + ` LEFT JOIN` + ` (SELECT preferred_login_name.user_id, preferred_login_name.login_name FROM projections.login_names as preferred_login_name WHERE preferred_login_name.is_primary = $1) as preferred_login_name` + - ` on preferred_login_name.user_id = projections.users.id` + ` on preferred_login_name.user_id = projections.users2.id` usersCols = []string{ "id", "creation_date", @@ -760,6 +823,155 @@ func Test_UserPrepares(t *testing.T) { }, object: nil, }, + { + name: "prepareNotifyUserQuery no result", + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { + return prepareNotifyUserQuery("instanceID") + }, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(notifyUserQuery), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*NotifyUser)(nil), + }, + { + name: "prepareNotifyUserQuery notify found", + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { + return prepareNotifyUserQuery("instanceID") + }, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(notifyUserQuery), + notifyUserCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + domain.UserStateActive, + domain.UserTypeHuman, + "username", + pq.StringArray{"login_name1", "login_name2"}, + "login_name1", + //human + "id", + "first_name", + "last_name", + "nick_name", + "display_name", + "de", + domain.GenderUnspecified, + "avatar_key", + //notify + "id", + "lastEmail", + "verifiedEmail", + "lastPhone", + "verifiedPhone", + true, + }, + ), + }, + object: &NotifyUser{ + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + State: domain.UserStateActive, + Type: domain.UserTypeHuman, + Username: "username", + LoginNames: []string{"login_name1", "login_name2"}, + PreferredLoginName: "login_name1", + FirstName: "first_name", + LastName: "last_name", + NickName: "nick_name", + DisplayName: "display_name", + AvatarKey: "avatar_key", + PreferredLanguage: language.German, + Gender: domain.GenderUnspecified, + LastEmail: "lastEmail", + VerifiedEmail: "verifiedEmail", + LastPhone: "lastPhone", + VerifiedPhone: "verifiedPhone", + PasswordSet: true, + }, + }, + { + name: "prepareNotifyUserQuery not notify found (error)", + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { + return prepareNotifyUserQuery("instanceID") + }, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(notifyUserQuery), + notifyUserCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + domain.UserStateActive, + domain.UserTypeHuman, + "username", + pq.StringArray{"login_name1", "login_name2"}, + "login_name1", + //human + "id", + "first_name", + "last_name", + "nick_name", + "display_name", + "de", + domain.GenderUnspecified, + "avatar_key", + nil, + nil, + nil, + nil, + nil, + nil, + }, + ), + err: func(err error) (error, bool) { + if !errs.IsPreconditionFailed(err) { + return fmt.Errorf("err should be zitadel.PredconditionError got: %w", err), false + } + return nil, true + }, + }, + object: (*NotifyUser)(nil), + }, + { + name: "prepareNotifyUserQuery sql err", + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { + return prepareNotifyUserQuery("instanceID") + }, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(notifyUserQuery), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, { name: "prepareUsersQuery no result", prepare: prepareUsersQuery,