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 <silvan.reusser@gmail.com>
This commit is contained in:
Livio Spring 2022-07-06 14:09:49 +02:00 committed by GitHub
parent d15a15c809
commit a1d404291d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2018 additions and 1839 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
package repository
type Repository interface {
Health() error
}

View File

@ -21,7 +21,7 @@ type TemplateData struct {
Subject string
Greeting string
Text string
Href string
URL string
ButtonText string
PrimaryColor string
BackgroundColor string

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 " +

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
&notifyUserID,
&notifyEmail,
&notifyVerifiedEmail,
&notifyPhone,
&notifyVerifiedPhone,
&notifyPasswordSet,
)
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(),

View File

@ -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" +

View File

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