mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 19:07:26 +00:00
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:
parent
d15a15c809
commit
a1d404291d
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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")
|
||||
}
|
663
internal/notification/projection.go
Normal file
663
internal/notification/projection.go
Normal 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})
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package repository
|
||||
|
||||
type Repository interface {
|
||||
Health() error
|
||||
}
|
@ -21,7 +21,7 @@ type TemplateData struct {
|
||||
Subject string
|
||||
Greeting string
|
||||
Text string
|
||||
Href string
|
||||
URL string
|
||||
ButtonText string
|
||||
PrimaryColor string
|
||||
BackgroundColor string
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
73
internal/notification/types/notification.go
Normal file
73
internal/notification/types/notification.go
Normal 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"
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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 " +
|
||||
|
@ -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")
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -11,9 +11,7 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
)
|
||||
@ -92,6 +90,31 @@ type Machine struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
type NotifyUser struct {
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
Sequence uint64
|
||||
State domain.UserState
|
||||
Type domain.UserType
|
||||
Username string
|
||||
LoginNames []string
|
||||
PreferredLoginName string
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
DisplayName string
|
||||
AvatarKey string
|
||||
PreferredLanguage language.Tag
|
||||
Gender domain.Gender
|
||||
LastEmail string
|
||||
VerifiedEmail string
|
||||
LastPhone string
|
||||
VerifiedPhone string
|
||||
PasswordSet bool
|
||||
}
|
||||
|
||||
type UserSearchQueries struct {
|
||||
SearchRequest
|
||||
Queries []SearchQuery
|
||||
@ -237,6 +260,38 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
notifyTable = table{
|
||||
name: projection.UserNotifyTable,
|
||||
}
|
||||
NotifyUserIDCol = Column{
|
||||
name: projection.NotifyUserIDCol,
|
||||
table: notifyTable,
|
||||
}
|
||||
NotifyEmailCol = Column{
|
||||
name: projection.NotifyLastEmailCol,
|
||||
table: notifyTable,
|
||||
isOrderByLower: true,
|
||||
}
|
||||
NotifyVerifiedEmailCol = Column{
|
||||
name: projection.NotifyVerifiedEmailCol,
|
||||
table: notifyTable,
|
||||
isOrderByLower: true,
|
||||
}
|
||||
NotifyPhoneCol = Column{
|
||||
name: projection.NotifyLastPhoneCol,
|
||||
table: notifyTable,
|
||||
}
|
||||
NotifyVerifiedPhoneCol = Column{
|
||||
name: projection.NotifyVerifiedPhoneCol,
|
||||
table: notifyTable,
|
||||
}
|
||||
NotifyPasswordSetCol = Column{
|
||||
name: projection.NotifyPasswordSetCol,
|
||||
table: notifyTable,
|
||||
}
|
||||
)
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, shouldTriggered bool, userID string, queries ...SearchQuery) (*User, error) {
|
||||
if shouldTriggered {
|
||||
projection.UserProjection.TriggerBulk(ctx)
|
||||
@ -327,6 +382,28 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S
|
||||
return scan(row)
|
||||
}
|
||||
|
||||
func (q *Queries) GeNotifyUser(ctx context.Context, shouldTriggered bool, userID string, queries ...SearchQuery) (*NotifyUser, error) {
|
||||
if shouldTriggered {
|
||||
projection.UserProjection.TriggerBulk(ctx)
|
||||
}
|
||||
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
query, scan := prepareNotifyUserQuery(instanceID)
|
||||
for _, q := range queries {
|
||||
query = q.toQuery(query)
|
||||
}
|
||||
stmt, args, err := query.Where(sq.Eq{
|
||||
UserIDCol.identifier(): userID,
|
||||
UserInstanceIDCol.identifier(): instanceID,
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatment")
|
||||
}
|
||||
|
||||
row := q.client.QueryRowContext(ctx, stmt, args...)
|
||||
return scan(row)
|
||||
}
|
||||
|
||||
func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries) (*Users, error) {
|
||||
query, scan := prepareUsersQuery()
|
||||
stmt, args, err := queries.toQuery(query).
|
||||
@ -748,6 +825,143 @@ func preparePhoneQuery() (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) {
|
||||
}
|
||||
}
|
||||
|
||||
func prepareNotifyUserQuery(instanceID string) (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) {
|
||||
loginNamesQuery, loginNamesArgs, err := sq.Select(
|
||||
userLoginNamesUserIDCol.identifier(),
|
||||
"ARRAY_AGG("+userLoginNamesNameCol.identifier()+") as "+userLoginNamesListCol.name).
|
||||
From(userLoginNamesTable.identifier()).
|
||||
GroupBy(userLoginNamesUserIDCol.identifier()).
|
||||
Where(sq.Eq{
|
||||
userLoginNamesInstanceIDCol.identifier(): instanceID,
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return sq.SelectBuilder{}, nil
|
||||
}
|
||||
preferredLoginNameQuery, preferredLoginNameArgs, err := sq.Select(
|
||||
userPreferredLoginNameUserIDCol.identifier(),
|
||||
userPreferredLoginNameCol.identifier()).
|
||||
From(userPreferredLoginNameTable.identifier()).
|
||||
Where(sq.Eq{
|
||||
userPreferredLoginNameIsPrimaryCol.identifier(): true,
|
||||
userPreferredLoginNameInstanceIDCol.identifier(): instanceID,
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return sq.SelectBuilder{}, nil
|
||||
}
|
||||
return sq.Select(
|
||||
UserIDCol.identifier(),
|
||||
UserCreationDateCol.identifier(),
|
||||
UserChangeDateCol.identifier(),
|
||||
UserResourceOwnerCol.identifier(),
|
||||
UserSequenceCol.identifier(),
|
||||
UserStateCol.identifier(),
|
||||
UserTypeCol.identifier(),
|
||||
UserUsernameCol.identifier(),
|
||||
userLoginNamesListCol.identifier(),
|
||||
userPreferredLoginNameCol.identifier(),
|
||||
HumanUserIDCol.identifier(),
|
||||
HumanFirstNameCol.identifier(),
|
||||
HumanLastNameCol.identifier(),
|
||||
HumanNickNameCol.identifier(),
|
||||
HumanDisplayNameCol.identifier(),
|
||||
HumanPreferredLanguageCol.identifier(),
|
||||
HumanGenderCol.identifier(),
|
||||
HumanAvatarURLCol.identifier(),
|
||||
NotifyUserIDCol.identifier(),
|
||||
NotifyEmailCol.identifier(),
|
||||
NotifyVerifiedEmailCol.identifier(),
|
||||
NotifyPhoneCol.identifier(),
|
||||
NotifyVerifiedPhoneCol.identifier(),
|
||||
NotifyPasswordSetCol.identifier(),
|
||||
).
|
||||
From(userTable.identifier()).
|
||||
LeftJoin(join(HumanUserIDCol, UserIDCol)).
|
||||
LeftJoin(join(NotifyUserIDCol, UserIDCol)).
|
||||
LeftJoin("("+loginNamesQuery+") as "+userLoginNamesTable.alias+" on "+userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier(), loginNamesArgs...).
|
||||
LeftJoin("("+preferredLoginNameQuery+") as "+userPreferredLoginNameTable.alias+" on "+userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier(), preferredLoginNameArgs...).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (*NotifyUser, error) {
|
||||
u := new(NotifyUser)
|
||||
loginNames := pq.StringArray{}
|
||||
preferredLoginName := sql.NullString{}
|
||||
|
||||
humanID := sql.NullString{}
|
||||
firstName := sql.NullString{}
|
||||
lastName := sql.NullString{}
|
||||
nickName := sql.NullString{}
|
||||
displayName := sql.NullString{}
|
||||
preferredLanguage := sql.NullString{}
|
||||
gender := sql.NullInt32{}
|
||||
avatarKey := sql.NullString{}
|
||||
|
||||
notifyUserID := sql.NullString{}
|
||||
notifyEmail := sql.NullString{}
|
||||
notifyVerifiedEmail := sql.NullString{}
|
||||
notifyPhone := sql.NullString{}
|
||||
notifyVerifiedPhone := sql.NullString{}
|
||||
notifyPasswordSet := sql.NullBool{}
|
||||
|
||||
err := row.Scan(
|
||||
&u.ID,
|
||||
&u.CreationDate,
|
||||
&u.ChangeDate,
|
||||
&u.ResourceOwner,
|
||||
&u.Sequence,
|
||||
&u.State,
|
||||
&u.Type,
|
||||
&u.Username,
|
||||
&loginNames,
|
||||
&preferredLoginName,
|
||||
&humanID,
|
||||
&firstName,
|
||||
&lastName,
|
||||
&nickName,
|
||||
&displayName,
|
||||
&preferredLanguage,
|
||||
&gender,
|
||||
&avatarKey,
|
||||
¬ifyUserID,
|
||||
¬ifyEmail,
|
||||
¬ifyVerifiedEmail,
|
||||
¬ifyPhone,
|
||||
¬ifyVerifiedPhone,
|
||||
¬ifyPasswordSet,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if errs.Is(err, sql.ErrNoRows) {
|
||||
return nil, errors.ThrowNotFound(err, "QUERY-Dgqd2", "Errors.User.NotFound")
|
||||
}
|
||||
return nil, errors.ThrowInternal(err, "QUERY-Dbwsg", "Errors.Internal")
|
||||
}
|
||||
|
||||
if !notifyUserID.Valid {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "QUERY-Sfw3f", "Errors.User.NotFound")
|
||||
}
|
||||
|
||||
u.LoginNames = loginNames
|
||||
if preferredLoginName.Valid {
|
||||
u.PreferredLoginName = preferredLoginName.String
|
||||
}
|
||||
if humanID.Valid {
|
||||
u.FirstName = firstName.String
|
||||
u.LastName = lastName.String
|
||||
u.NickName = nickName.String
|
||||
u.DisplayName = displayName.String
|
||||
u.AvatarKey = avatarKey.String
|
||||
u.PreferredLanguage = language.Make(preferredLanguage.String)
|
||||
u.Gender = domain.Gender(gender.Int32)
|
||||
}
|
||||
u.LastEmail = notifyEmail.String
|
||||
u.VerifiedEmail = notifyVerifiedEmail.String
|
||||
u.LastPhone = notifyPhone.String
|
||||
u.VerifiedPhone = notifyVerifiedPhone.String
|
||||
u.PasswordSet = notifyPasswordSet.Bool
|
||||
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
|
||||
func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) {
|
||||
return sq.Select(
|
||||
UserIDCol.identifier(),
|
||||
|
@ -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" +
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user