mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 13:37:35 +00:00
chore: move the go code into a subfolder
This commit is contained in:
50
apps/api/internal/notification/handlers/already_handled.go
Normal file
50
apps/api/internal/notification/handlers/already_handled.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
type alreadyHandled struct {
|
||||
event eventstore.Event
|
||||
eventTypes []eventstore.EventType
|
||||
data map[string]interface{}
|
||||
|
||||
handled bool
|
||||
}
|
||||
|
||||
func (a *alreadyHandled) Reduce() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *alreadyHandled) AppendEvents(event ...eventstore.Event) {
|
||||
if len(event) > 0 {
|
||||
a.handled = true
|
||||
}
|
||||
}
|
||||
|
||||
func (a *alreadyHandled) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
InstanceID(a.event.Aggregate().InstanceID).
|
||||
SequenceGreater(a.event.Sequence()).
|
||||
AddQuery().
|
||||
AggregateTypes(a.event.Aggregate().Type).
|
||||
AggregateIDs(a.event.Aggregate().ID).
|
||||
EventTypes(a.eventTypes...).
|
||||
EventData(a.data).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func (n *NotificationQueries) IsAlreadyHandled(ctx context.Context, event eventstore.Event, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) {
|
||||
already := &alreadyHandled{
|
||||
event: event,
|
||||
eventTypes: eventTypes,
|
||||
data: data,
|
||||
}
|
||||
err := n.es.FilterToQueryReducer(ctx, already)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return already.handled, nil
|
||||
}
|
250
apps/api/internal/notification/handlers/back_channel_logout.go
Normal file
250
apps/api/internal/notification/handlers/back_channel_logout.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
zoidc "github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
zcrypto "github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/set"
|
||||
_ "github.com/zitadel/zitadel/internal/notification/statik"
|
||||
"github.com/zitadel/zitadel/internal/notification/types"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/sessionlogout"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
BackChannelLogoutNotificationsProjectionTable = "projections.notifications_back_channel_logout"
|
||||
)
|
||||
|
||||
type backChannelLogoutNotifier struct {
|
||||
commands *command.Commands
|
||||
queries *NotificationQueries
|
||||
eventstore *eventstore.Eventstore
|
||||
keyEncryptionAlg zcrypto.EncryptionAlgorithm
|
||||
channels types.ChannelChains
|
||||
idGenerator id.Generator
|
||||
tokenLifetime time.Duration
|
||||
}
|
||||
|
||||
func NewBackChannelLogoutNotifier(
|
||||
ctx context.Context,
|
||||
config handler.Config,
|
||||
commands *command.Commands,
|
||||
queries *NotificationQueries,
|
||||
es *eventstore.Eventstore,
|
||||
keyEncryptionAlg zcrypto.EncryptionAlgorithm,
|
||||
channels types.ChannelChains,
|
||||
tokenLifetime time.Duration,
|
||||
) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, &backChannelLogoutNotifier{
|
||||
commands: commands,
|
||||
queries: queries,
|
||||
eventstore: es,
|
||||
keyEncryptionAlg: keyEncryptionAlg,
|
||||
channels: channels,
|
||||
tokenLifetime: tokenLifetime,
|
||||
idGenerator: id.SonyFlakeGenerator(),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (*backChannelLogoutNotifier) Name() string {
|
||||
return BackChannelLogoutNotificationsProjectionTable
|
||||
}
|
||||
|
||||
func (u *backChannelLogoutNotifier) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: session.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: session.TerminateType,
|
||||
Reduce: u.reduceSessionTerminated,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Aggregate: user.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: user.HumanSignedOutType,
|
||||
Reduce: u.reduceUserSignedOut,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (u *backChannelLogoutNotifier) reduceUserSignedOut(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanSignedOutEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Gr63h", "reduce.wrong.event.type %s", user.HumanSignedOutType)
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx, err := u.queries.HandlerContext(ctx, event.Aggregate())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !authz.GetFeatures(ctx).EnableBackChannelLogout {
|
||||
return nil
|
||||
}
|
||||
if e.SessionID == "" {
|
||||
return nil
|
||||
}
|
||||
return u.terminateSession(ctx, e.SessionID, e)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *backChannelLogoutNotifier) reduceSessionTerminated(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.TerminateEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-D6H2h", "reduce.wrong.event.type %s", session.TerminateType)
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx, err := u.queries.HandlerContext(ctx, event.Aggregate())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !authz.GetFeatures(ctx).EnableBackChannelLogout {
|
||||
return nil
|
||||
}
|
||||
return u.terminateSession(ctx, e.Aggregate().ID, e)
|
||||
}), nil
|
||||
}
|
||||
|
||||
type backChannelLogoutSession struct {
|
||||
sessionID string
|
||||
|
||||
// sessions contain a map of oidc session IDs and their corresponding clientID
|
||||
sessions []backChannelLogoutOIDCSessions
|
||||
}
|
||||
|
||||
func (u *backChannelLogoutNotifier) terminateSession(ctx context.Context, id string, e eventstore.Event) error {
|
||||
sessions := &backChannelLogoutSession{sessionID: id}
|
||||
err := u.eventstore.FilterToQueryReducer(ctx, sessions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
getSigner := zoidc.GetSignerOnce(u.queries.GetActiveSigningWebKey)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(sessions.sessions))
|
||||
errs := make([]error, 0, len(sessions.sessions))
|
||||
for _, oidcSession := range sessions.sessions {
|
||||
go func(oidcSession *backChannelLogoutOIDCSessions) {
|
||||
defer wg.Done()
|
||||
err := u.sendLogoutToken(ctx, oidcSession, e, getSigner)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return
|
||||
}
|
||||
err = u.commands.BackChannelLogoutSent(ctx, oidcSession.SessionID, oidcSession.OIDCSessionID, e.Aggregate().InstanceID)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}(&oidcSession)
|
||||
}
|
||||
wg.Wait()
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (u *backChannelLogoutNotifier) sendLogoutToken(ctx context.Context, oidcSession *backChannelLogoutOIDCSessions, e eventstore.Event, getSigner zoidc.SignerFunc) error {
|
||||
token, err := u.logoutToken(ctx, oidcSession, getSigner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendSecurityTokenEvent(ctx, set.Config{CallURL: oidcSession.BackChannelLogoutURI}, u.channels, &LogoutTokenMessage{LogoutToken: token}, e.Type()).WithoutTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *backChannelLogoutNotifier) logoutToken(ctx context.Context, oidcSession *backChannelLogoutOIDCSessions, getSigner zoidc.SignerFunc) (string, error) {
|
||||
jwtID, err := u.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token := oidc.NewLogoutTokenClaims(
|
||||
http_utils.DomainContext(ctx).Origin(),
|
||||
oidcSession.UserID,
|
||||
oidc.Audience{oidcSession.ClientID},
|
||||
time.Now().Add(u.tokenLifetime),
|
||||
jwtID,
|
||||
oidcSession.SessionID,
|
||||
time.Second,
|
||||
)
|
||||
signer, _, err := getSigner(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return crypto.Sign(token, signer)
|
||||
}
|
||||
|
||||
type LogoutTokenMessage struct {
|
||||
LogoutToken string `schema:"logout_token"`
|
||||
}
|
||||
|
||||
type backChannelLogoutOIDCSessions struct {
|
||||
SessionID string
|
||||
OIDCSessionID string
|
||||
UserID string
|
||||
ClientID string
|
||||
BackChannelLogoutURI string
|
||||
}
|
||||
|
||||
func (b *backChannelLogoutSession) Reduce() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *backChannelLogoutSession) AppendEvents(events ...eventstore.Event) {
|
||||
for _, event := range events {
|
||||
switch e := event.(type) {
|
||||
case *sessionlogout.BackChannelLogoutRegisteredEvent:
|
||||
b.sessions = append(b.sessions, backChannelLogoutOIDCSessions{
|
||||
SessionID: b.sessionID,
|
||||
OIDCSessionID: e.OIDCSessionID,
|
||||
UserID: e.UserID,
|
||||
ClientID: e.ClientID,
|
||||
BackChannelLogoutURI: e.BackChannelLogoutURI,
|
||||
})
|
||||
case *sessionlogout.BackChannelLogoutSentEvent:
|
||||
b.sessions = slices.DeleteFunc(b.sessions, func(session backChannelLogoutOIDCSessions) bool {
|
||||
return session.OIDCSessionID == e.OIDCSessionID
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backChannelLogoutSession) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(sessionlogout.AggregateType).
|
||||
AggregateIDs(b.sessionID).
|
||||
EventTypes(
|
||||
sessionlogout.BackChannelLogoutRegisteredType,
|
||||
sessionlogout.BackChannelLogoutSentType).
|
||||
Builder()
|
||||
}
|
26
apps/api/internal/notification/handlers/commands.go
Normal file
26
apps/api/internal/notification/handlers/commands.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/notification/senders"
|
||||
"github.com/zitadel/zitadel/internal/repository/milestone"
|
||||
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||
)
|
||||
|
||||
type Commands interface {
|
||||
HumanInitCodeSent(ctx context.Context, orgID, userID string) error
|
||||
HumanEmailVerificationCodeSent(ctx context.Context, orgID, userID string) error
|
||||
PasswordCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error
|
||||
HumanOTPSMSCodeSent(ctx context.Context, userID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error
|
||||
HumanOTPEmailCodeSent(ctx context.Context, userID, resourceOwner string) error
|
||||
OTPSMSSent(ctx context.Context, sessionID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error
|
||||
OTPEmailSent(ctx context.Context, sessionID, resourceOwner string) error
|
||||
UserDomainClaimedSent(ctx context.Context, orgID, userID string) error
|
||||
HumanPasswordlessInitCodeSent(ctx context.Context, userID, resourceOwner, codeID string) error
|
||||
PasswordChangeSent(ctx context.Context, orgID, userID string) error
|
||||
HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error
|
||||
InviteCodeSent(ctx context.Context, orgID, userID string) error
|
||||
UsageNotificationSent(ctx context.Context, dueEvent *quota.NotificationDueEvent) error
|
||||
MilestonePushed(ctx context.Context, instanceID string, msType milestone.Type, endpoints []string) error
|
||||
}
|
59
apps/api/internal/notification/handlers/config_email.go
Normal file
59
apps/api/internal/notification/handlers/config_email.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/email"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
// GetSMTPConfig reads the iam SMTP provider config
|
||||
func (n *NotificationQueries) GetActiveEmailConfig(ctx context.Context) (*email.Config, error) {
|
||||
config, err := n.SMTPConfigActive(ctx, authz.GetInstance(ctx).InstanceID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider := &email.Provider{
|
||||
ID: config.ID,
|
||||
Description: config.Description,
|
||||
}
|
||||
if config.SMTPConfig != nil {
|
||||
if config.SMTPConfig.Password == nil {
|
||||
return nil, zerrors.ThrowNotFound(err, "QUERY-Wrs3gw", "Errors.SMTPConfig.NotFound")
|
||||
}
|
||||
password, err := crypto.DecryptString(config.SMTPConfig.Password, n.SMTPPasswordCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &email.Config{
|
||||
ProviderConfig: provider,
|
||||
SMTPConfig: &smtp.Config{
|
||||
From: config.SMTPConfig.SenderAddress,
|
||||
FromName: config.SMTPConfig.SenderName,
|
||||
ReplyToAddress: config.SMTPConfig.ReplyToAddress,
|
||||
Tls: config.SMTPConfig.TLS,
|
||||
SMTP: smtp.SMTP{
|
||||
Host: config.SMTPConfig.Host,
|
||||
User: config.SMTPConfig.User,
|
||||
Password: password,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if config.HTTPConfig != nil {
|
||||
return &email.Config{
|
||||
ProviderConfig: provider,
|
||||
WebhookConfig: &webhook.Config{
|
||||
CallURL: config.HTTPConfig.Endpoint,
|
||||
Method: http.MethodPost,
|
||||
Headers: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return nil, zerrors.ThrowNotFound(err, "QUERY-KPQleOckOV", "Errors.SMTPConfig.NotFound")
|
||||
}
|
21
apps/api/internal/notification/handlers/config_filesystem.go
Normal file
21
apps/api/internal/notification/handlers/config_filesystem.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
)
|
||||
|
||||
// GetFileSystemProvider reads the iam filesystem provider config
|
||||
func (n *NotificationQueries) GetFileSystemProvider(ctx context.Context) (*fs.Config, error) {
|
||||
config, err := n.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fs.Config{
|
||||
Compact: config.Compact,
|
||||
Path: n.fileSystemPath,
|
||||
}, nil
|
||||
}
|
20
apps/api/internal/notification/handlers/config_log.go
Normal file
20
apps/api/internal/notification/handlers/config_log.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
)
|
||||
|
||||
// GetLogProvider reads the iam log provider config
|
||||
func (n *NotificationQueries) GetLogProvider(ctx context.Context) (*log.Config, error) {
|
||||
config, err := n.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeLog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &log.Config{
|
||||
Compact: config.Compact,
|
||||
}, nil
|
||||
}
|
56
apps/api/internal/notification/handlers/config_sms.go
Normal file
56
apps/api/internal/notification/handlers/config_sms.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/sms"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
// GetActiveSMSConfig reads the active iam sms provider config
|
||||
func (n *NotificationQueries) GetActiveSMSConfig(ctx context.Context) (*sms.Config, error) {
|
||||
config, err := n.SMSProviderConfigActive(ctx, authz.GetInstance(ctx).InstanceID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider := &sms.Provider{
|
||||
ID: config.ID,
|
||||
Description: config.Description,
|
||||
}
|
||||
if config.TwilioConfig != nil {
|
||||
if config.TwilioConfig.Token == nil {
|
||||
return nil, zerrors.ThrowNotFound(err, "QUERY-SFefsd", "Errors.SMS.Twilio.NotFound")
|
||||
}
|
||||
token, err := crypto.DecryptString(config.TwilioConfig.Token, n.SMSTokenCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sms.Config{
|
||||
ProviderConfig: provider,
|
||||
TwilioConfig: &twilio.Config{
|
||||
SID: config.TwilioConfig.SID,
|
||||
Token: token,
|
||||
SenderNumber: config.TwilioConfig.SenderNumber,
|
||||
VerifyServiceSID: config.TwilioConfig.VerifyServiceSID,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if config.HTTPConfig != nil {
|
||||
return &sms.Config{
|
||||
ProviderConfig: provider,
|
||||
WebhookConfig: &webhook.Config{
|
||||
CallURL: config.HTTPConfig.Endpoint,
|
||||
Method: http.MethodPost,
|
||||
Headers: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, zerrors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound")
|
||||
}
|
28
apps/api/internal/notification/handlers/ctx.go
Normal file
28
apps/api/internal/notification/handlers/ctx.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const NotifyUserID = "NOTIFICATION" //TODO: system?
|
||||
|
||||
func HandlerContext(parent context.Context, event *eventstore.Aggregate) context.Context {
|
||||
ctx := authz.WithInstanceID(parent, event.InstanceID)
|
||||
return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: event.ResourceOwner})
|
||||
}
|
||||
|
||||
func ContextWithNotifier(ctx context.Context, aggregate *eventstore.Aggregate) context.Context {
|
||||
return authz.WithInstanceID(authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: aggregate.ResourceOwner}), aggregate.InstanceID)
|
||||
}
|
||||
|
||||
func (n *NotificationQueries) HandlerContext(parent context.Context, event *eventstore.Aggregate) (context.Context, error) {
|
||||
instance, err := n.InstanceByID(parent, event.InstanceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := authz.WithInstance(parent, instance)
|
||||
return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: event.ResourceOwner}), nil
|
||||
}
|
5
apps/api/internal/notification/handlers/gen_mock.go
Normal file
5
apps/api/internal/notification/handlers/gen_mock.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package handlers
|
||||
|
||||
//go:generate mockgen -package mock -destination ./mock/queries.mock.go github.com/zitadel/zitadel/internal/notification/handlers Queries
|
||||
//go:generate mockgen -package mock -destination ./mock/commands.mock.go github.com/zitadel/zitadel/internal/notification/handlers Commands
|
||||
//go:generate mockgen -package mock -destination ./mock/queue.mock.go github.com/zitadel/zitadel/internal/notification/handlers Queue
|
@@ -0,0 +1,23 @@
|
||||
//go:build integration
|
||||
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
CTX context.Context
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||
defer cancel()
|
||||
CTX = ctx
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
@@ -0,0 +1,159 @@
|
||||
//go:build integration
|
||||
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/integration/sink"
|
||||
"github.com/zitadel/zitadel/internal/repository/milestone"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/app"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object"
|
||||
oidc_v2 "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/project"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
)
|
||||
|
||||
func TestServer_TelemetryPushMilestones(t *testing.T) {
|
||||
sub := sink.Subscribe(CTX, sink.ChannelMilestone)
|
||||
defer sub.Close()
|
||||
|
||||
instance := integration.NewInstance(CTX)
|
||||
iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
||||
t.Log("testing against instance", instance.ID())
|
||||
awaitMilestone(t, sub, instance.ID(), milestone.InstanceCreated)
|
||||
|
||||
projectAdded, err := instance.Client.Mgmt.AddProject(iamOwnerCtx, &management.AddProjectRequest{Name: "integration"})
|
||||
require.NoError(t, err)
|
||||
awaitMilestone(t, sub, instance.ID(), milestone.ProjectCreated)
|
||||
|
||||
redirectURI := "http://localhost:8888"
|
||||
application, err := instance.Client.Mgmt.AddOIDCApp(iamOwnerCtx, &management.AddOIDCAppRequest{
|
||||
ProjectId: projectAdded.GetId(),
|
||||
Name: "integration",
|
||||
RedirectUris: []string{redirectURI},
|
||||
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
|
||||
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
|
||||
AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
|
||||
AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
|
||||
DevMode: true,
|
||||
AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
awaitMilestone(t, sub, instance.ID(), milestone.ApplicationCreated)
|
||||
|
||||
// create the session to be used for the authN of the clients
|
||||
sessionID, sessionToken, _, _ := instance.CreatePasswordSession(t, iamOwnerCtx, instance.AdminUserID, "Password1!")
|
||||
|
||||
console := consoleOIDCConfig(t, instance)
|
||||
loginToClient(t, instance, console.GetClientId(), console.GetRedirectUris()[0], sessionID, sessionToken)
|
||||
awaitMilestone(t, sub, instance.ID(), milestone.AuthenticationSucceededOnInstance)
|
||||
|
||||
// make sure the client has been projected
|
||||
require.EventuallyWithT(t, func(collectT *assert.CollectT) {
|
||||
_, err := instance.Client.Mgmt.GetAppByID(iamOwnerCtx, &management.GetAppByIDRequest{
|
||||
ProjectId: projectAdded.GetId(),
|
||||
AppId: application.GetAppId(),
|
||||
})
|
||||
assert.NoError(collectT, err)
|
||||
}, time.Minute, time.Second, "app not found")
|
||||
loginToClient(t, instance, application.GetClientId(), redirectURI, sessionID, sessionToken)
|
||||
awaitMilestone(t, sub, instance.ID(), milestone.AuthenticationSucceededOnApplication)
|
||||
|
||||
_, err = integration.SystemClient().RemoveInstance(CTX, &system.RemoveInstanceRequest{InstanceId: instance.ID()})
|
||||
require.NoError(t, err)
|
||||
awaitMilestone(t, sub, instance.ID(), milestone.InstanceDeleted)
|
||||
}
|
||||
|
||||
func loginToClient(t *testing.T, instance *integration.Instance, clientID, redirectURI, sessionID, sessionToken string) {
|
||||
iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
||||
|
||||
_, authRequestID, err := instance.CreateOIDCAuthRequestWithDomain(iamOwnerCtx, instance.Domain, clientID, instance.Users.Get(integration.UserTypeIAMOwner).ID, redirectURI, "openid")
|
||||
require.NoError(t, err)
|
||||
callback, err := instance.Client.OIDCv2.CreateCallback(iamOwnerCtx, &oidc_v2.CreateCallbackRequest{
|
||||
AuthRequestId: authRequestID,
|
||||
CallbackKind: &oidc_v2.CreateCallbackRequest_Session{Session: &oidc_v2.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
provider, err := instance.CreateRelyingPartyForDomain(iamOwnerCtx, instance.Domain, clientID, redirectURI, instance.Users.Get(integration.UserTypeLogin).Username)
|
||||
require.NoError(t, err)
|
||||
callbackURL, err := url.Parse(callback.GetCallbackUrl())
|
||||
require.NoError(t, err)
|
||||
code := callbackURL.Query().Get("code")
|
||||
_, err = rp.CodeExchange[*oidc.IDTokenClaims](iamOwnerCtx, code, provider, rp.WithCodeVerifier(integration.CodeVerifier))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func consoleOIDCConfig(t *testing.T, instance *integration.Instance) *app.OIDCConfig {
|
||||
iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
||||
|
||||
projects, err := instance.Client.Mgmt.ListProjects(iamOwnerCtx, &management.ListProjectsRequest{
|
||||
Queries: []*project.ProjectQuery{
|
||||
{
|
||||
Query: &project.ProjectQuery_NameQuery{
|
||||
NameQuery: &project.ProjectNameQuery{
|
||||
Name: "ZITADEL",
|
||||
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, projects.GetResult(), 1)
|
||||
apps, err := instance.Client.Mgmt.ListApps(iamOwnerCtx, &management.ListAppsRequest{
|
||||
ProjectId: projects.GetResult()[0].GetId(),
|
||||
Queries: []*app.AppQuery{
|
||||
{
|
||||
Query: &app.AppQuery_NameQuery{
|
||||
NameQuery: &app.AppNameQuery{
|
||||
Name: "Console",
|
||||
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps.GetResult(), 1)
|
||||
return apps.GetResult()[0].GetOidcConfig()
|
||||
}
|
||||
|
||||
func awaitMilestone(t *testing.T, sub *sink.Subscription, instanceID string, expectMilestoneType milestone.Type) {
|
||||
for {
|
||||
select {
|
||||
case req := <-sub.Recv():
|
||||
plain := new(bytes.Buffer)
|
||||
if err := json.Indent(plain, req.Body, "", " "); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("received milestone", plain.String())
|
||||
milestone := struct {
|
||||
InstanceID string `json:"instanceId"`
|
||||
Type milestone.Type `json:"type"`
|
||||
}{}
|
||||
if err := json.Unmarshal(req.Body, &milestone); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if milestone.Type == expectMilestoneType && milestone.InstanceID == instanceID {
|
||||
return
|
||||
}
|
||||
case <-time.After(20 * time.Second):
|
||||
t.Fatalf("timed out waiting for milestone %s for instance %s", expectMilestoneType, instanceID)
|
||||
}
|
||||
}
|
||||
}
|
240
apps/api/internal/notification/handlers/mock/commands.mock.go
Normal file
240
apps/api/internal/notification/handlers/mock/commands.mock.go
Normal file
@@ -0,0 +1,240 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/zitadel/zitadel/internal/notification/handlers (interfaces: Commands)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package mock -destination ./mock/commands.mock.go github.com/zitadel/zitadel/internal/notification/handlers Commands
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
senders "github.com/zitadel/zitadel/internal/notification/senders"
|
||||
milestone "github.com/zitadel/zitadel/internal/repository/milestone"
|
||||
quota "github.com/zitadel/zitadel/internal/repository/quota"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockCommands is a mock of Commands interface.
|
||||
type MockCommands struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockCommandsMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockCommandsMockRecorder is the mock recorder for MockCommands.
|
||||
type MockCommandsMockRecorder struct {
|
||||
mock *MockCommands
|
||||
}
|
||||
|
||||
// NewMockCommands creates a new mock instance.
|
||||
func NewMockCommands(ctrl *gomock.Controller) *MockCommands {
|
||||
mock := &MockCommands{ctrl: ctrl}
|
||||
mock.recorder = &MockCommandsMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockCommands) EXPECT() *MockCommandsMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// HumanEmailVerificationCodeSent mocks base method.
|
||||
func (m *MockCommands) HumanEmailVerificationCodeSent(ctx context.Context, orgID, userID string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HumanEmailVerificationCodeSent", ctx, orgID, userID)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent.
|
||||
func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(ctx, orgID, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanEmailVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanEmailVerificationCodeSent), ctx, orgID, userID)
|
||||
}
|
||||
|
||||
// HumanInitCodeSent mocks base method.
|
||||
func (m *MockCommands) HumanInitCodeSent(ctx context.Context, orgID, userID string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HumanInitCodeSent", ctx, orgID, userID)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HumanInitCodeSent indicates an expected call of HumanInitCodeSent.
|
||||
func (mr *MockCommandsMockRecorder) HumanInitCodeSent(ctx, orgID, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanInitCodeSent), ctx, orgID, userID)
|
||||
}
|
||||
|
||||
// HumanOTPEmailCodeSent mocks base method.
|
||||
func (m *MockCommands) HumanOTPEmailCodeSent(ctx context.Context, userID, resourceOwner string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HumanOTPEmailCodeSent", ctx, userID, resourceOwner)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent.
|
||||
func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(ctx, userID, resourceOwner any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPEmailCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPEmailCodeSent), ctx, userID, resourceOwner)
|
||||
}
|
||||
|
||||
// HumanOTPSMSCodeSent mocks base method.
|
||||
func (m *MockCommands) HumanOTPSMSCodeSent(ctx context.Context, userID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HumanOTPSMSCodeSent", ctx, userID, resourceOwner, generatorInfo)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent.
|
||||
func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(ctx, userID, resourceOwner, generatorInfo any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPSMSCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPSMSCodeSent), ctx, userID, resourceOwner, generatorInfo)
|
||||
}
|
||||
|
||||
// HumanPasswordlessInitCodeSent mocks base method.
|
||||
func (m *MockCommands) HumanPasswordlessInitCodeSent(ctx context.Context, userID, resourceOwner, codeID string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HumanPasswordlessInitCodeSent", ctx, userID, resourceOwner, codeID)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent.
|
||||
func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(ctx, userID, resourceOwner, codeID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPasswordlessInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPasswordlessInitCodeSent), ctx, userID, resourceOwner, codeID)
|
||||
}
|
||||
|
||||
// HumanPhoneVerificationCodeSent mocks base method.
|
||||
func (m *MockCommands) HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HumanPhoneVerificationCodeSent", ctx, orgID, userID, generatorInfo)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent.
|
||||
func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(ctx, orgID, userID, generatorInfo any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPhoneVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPhoneVerificationCodeSent), ctx, orgID, userID, generatorInfo)
|
||||
}
|
||||
|
||||
// InviteCodeSent mocks base method.
|
||||
func (m *MockCommands) InviteCodeSent(ctx context.Context, orgID, userID string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InviteCodeSent", ctx, orgID, userID)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// InviteCodeSent indicates an expected call of InviteCodeSent.
|
||||
func (mr *MockCommandsMockRecorder) InviteCodeSent(ctx, orgID, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InviteCodeSent", reflect.TypeOf((*MockCommands)(nil).InviteCodeSent), ctx, orgID, userID)
|
||||
}
|
||||
|
||||
// MilestonePushed mocks base method.
|
||||
func (m *MockCommands) MilestonePushed(ctx context.Context, instanceID string, msType milestone.Type, endpoints []string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "MilestonePushed", ctx, instanceID, msType, endpoints)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// MilestonePushed indicates an expected call of MilestonePushed.
|
||||
func (mr *MockCommandsMockRecorder) MilestonePushed(ctx, instanceID, msType, endpoints any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), ctx, instanceID, msType, endpoints)
|
||||
}
|
||||
|
||||
// OTPEmailSent mocks base method.
|
||||
func (m *MockCommands) OTPEmailSent(ctx context.Context, sessionID, resourceOwner string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OTPEmailSent", ctx, sessionID, resourceOwner)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// OTPEmailSent indicates an expected call of OTPEmailSent.
|
||||
func (mr *MockCommandsMockRecorder) OTPEmailSent(ctx, sessionID, resourceOwner any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPEmailSent", reflect.TypeOf((*MockCommands)(nil).OTPEmailSent), ctx, sessionID, resourceOwner)
|
||||
}
|
||||
|
||||
// OTPSMSSent mocks base method.
|
||||
func (m *MockCommands) OTPSMSSent(ctx context.Context, sessionID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OTPSMSSent", ctx, sessionID, resourceOwner, generatorInfo)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// OTPSMSSent indicates an expected call of OTPSMSSent.
|
||||
func (mr *MockCommandsMockRecorder) OTPSMSSent(ctx, sessionID, resourceOwner, generatorInfo any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPSMSSent", reflect.TypeOf((*MockCommands)(nil).OTPSMSSent), ctx, sessionID, resourceOwner, generatorInfo)
|
||||
}
|
||||
|
||||
// PasswordChangeSent mocks base method.
|
||||
func (m *MockCommands) PasswordChangeSent(ctx context.Context, orgID, userID string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PasswordChangeSent", ctx, orgID, userID)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PasswordChangeSent indicates an expected call of PasswordChangeSent.
|
||||
func (mr *MockCommandsMockRecorder) PasswordChangeSent(ctx, orgID, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordChangeSent", reflect.TypeOf((*MockCommands)(nil).PasswordChangeSent), ctx, orgID, userID)
|
||||
}
|
||||
|
||||
// PasswordCodeSent mocks base method.
|
||||
func (m *MockCommands) PasswordCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PasswordCodeSent", ctx, orgID, userID, generatorInfo)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PasswordCodeSent indicates an expected call of PasswordCodeSent.
|
||||
func (mr *MockCommandsMockRecorder) PasswordCodeSent(ctx, orgID, userID, generatorInfo any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), ctx, orgID, userID, generatorInfo)
|
||||
}
|
||||
|
||||
// UsageNotificationSent mocks base method.
|
||||
func (m *MockCommands) UsageNotificationSent(ctx context.Context, dueEvent *quota.NotificationDueEvent) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UsageNotificationSent", ctx, dueEvent)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UsageNotificationSent indicates an expected call of UsageNotificationSent.
|
||||
func (mr *MockCommandsMockRecorder) UsageNotificationSent(ctx, dueEvent any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UsageNotificationSent", reflect.TypeOf((*MockCommands)(nil).UsageNotificationSent), ctx, dueEvent)
|
||||
}
|
||||
|
||||
// UserDomainClaimedSent mocks base method.
|
||||
func (m *MockCommands) UserDomainClaimedSent(ctx context.Context, orgID, userID string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UserDomainClaimedSent", ctx, orgID, userID)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent.
|
||||
func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(ctx, orgID, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDomainClaimedSent", reflect.TypeOf((*MockCommands)(nil).UserDomainClaimedSent), ctx, orgID, userID)
|
||||
}
|
284
apps/api/internal/notification/handlers/mock/queries.mock.go
Normal file
284
apps/api/internal/notification/handlers/mock/queries.mock.go
Normal file
@@ -0,0 +1,284 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/zitadel/zitadel/internal/notification/handlers (interfaces: Queries)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package mock -destination ./mock/queries.mock.go github.com/zitadel/zitadel/internal/notification/handlers Queries
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
authz "github.com/zitadel/zitadel/internal/api/authz"
|
||||
domain "github.com/zitadel/zitadel/internal/domain"
|
||||
query "github.com/zitadel/zitadel/internal/query"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
language "golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// MockQueries is a mock of Queries interface.
|
||||
type MockQueries struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockQueriesMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockQueriesMockRecorder is the mock recorder for MockQueries.
|
||||
type MockQueriesMockRecorder struct {
|
||||
mock *MockQueries
|
||||
}
|
||||
|
||||
// NewMockQueries creates a new mock instance.
|
||||
func NewMockQueries(ctrl *gomock.Controller) *MockQueries {
|
||||
mock := &MockQueries{ctrl: ctrl}
|
||||
mock.recorder = &MockQueriesMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockQueries) EXPECT() *MockQueriesMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ActiveInstances mocks base method.
|
||||
func (m *MockQueries) ActiveInstances() []string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ActiveInstances")
|
||||
ret0, _ := ret[0].([]string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ActiveInstances indicates an expected call of ActiveInstances.
|
||||
func (mr *MockQueriesMockRecorder) ActiveInstances() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActiveInstances", reflect.TypeOf((*MockQueries)(nil).ActiveInstances))
|
||||
}
|
||||
|
||||
// ActiveLabelPolicyByOrg mocks base method.
|
||||
func (m *MockQueries) ActiveLabelPolicyByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.LabelPolicy, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ActiveLabelPolicyByOrg", ctx, orgID, withOwnerRemoved)
|
||||
ret0, _ := ret[0].(*query.LabelPolicy)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ActiveLabelPolicyByOrg indicates an expected call of ActiveLabelPolicyByOrg.
|
||||
func (mr *MockQueriesMockRecorder) ActiveLabelPolicyByOrg(ctx, orgID, withOwnerRemoved any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActiveLabelPolicyByOrg", reflect.TypeOf((*MockQueries)(nil).ActiveLabelPolicyByOrg), ctx, orgID, withOwnerRemoved)
|
||||
}
|
||||
|
||||
// CustomTextListByTemplate mocks base method.
|
||||
func (m *MockQueries) CustomTextListByTemplate(ctx context.Context, aggregateID, template string, withOwnerRemoved bool) (*query.CustomTexts, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CustomTextListByTemplate", ctx, aggregateID, template, withOwnerRemoved)
|
||||
ret0, _ := ret[0].(*query.CustomTexts)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CustomTextListByTemplate indicates an expected call of CustomTextListByTemplate.
|
||||
func (mr *MockQueriesMockRecorder) CustomTextListByTemplate(ctx, aggregateID, template, withOwnerRemoved any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CustomTextListByTemplate", reflect.TypeOf((*MockQueries)(nil).CustomTextListByTemplate), ctx, aggregateID, template, withOwnerRemoved)
|
||||
}
|
||||
|
||||
// GetActiveSigningWebKey mocks base method.
|
||||
func (m *MockQueries) GetActiveSigningWebKey(ctx context.Context) (*jose.JSONWebKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetActiveSigningWebKey", ctx)
|
||||
ret0, _ := ret[0].(*jose.JSONWebKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetActiveSigningWebKey indicates an expected call of GetActiveSigningWebKey.
|
||||
func (mr *MockQueriesMockRecorder) GetActiveSigningWebKey(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveSigningWebKey", reflect.TypeOf((*MockQueries)(nil).GetActiveSigningWebKey), ctx)
|
||||
}
|
||||
|
||||
// GetDefaultLanguage mocks base method.
|
||||
func (m *MockQueries) GetDefaultLanguage(ctx context.Context) language.Tag {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetDefaultLanguage", ctx)
|
||||
ret0, _ := ret[0].(language.Tag)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetDefaultLanguage indicates an expected call of GetDefaultLanguage.
|
||||
func (mr *MockQueriesMockRecorder) GetDefaultLanguage(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultLanguage", reflect.TypeOf((*MockQueries)(nil).GetDefaultLanguage), ctx)
|
||||
}
|
||||
|
||||
// GetInstanceRestrictions mocks base method.
|
||||
func (m *MockQueries) GetInstanceRestrictions(ctx context.Context) (query.Restrictions, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetInstanceRestrictions", ctx)
|
||||
ret0, _ := ret[0].(query.Restrictions)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetInstanceRestrictions indicates an expected call of GetInstanceRestrictions.
|
||||
func (mr *MockQueriesMockRecorder) GetInstanceRestrictions(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceRestrictions", reflect.TypeOf((*MockQueries)(nil).GetInstanceRestrictions), ctx)
|
||||
}
|
||||
|
||||
// GetNotifyUserByID mocks base method.
|
||||
func (m *MockQueries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string) (*query.NotifyUser, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetNotifyUserByID", ctx, shouldTriggered, userID)
|
||||
ret0, _ := ret[0].(*query.NotifyUser)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetNotifyUserByID indicates an expected call of GetNotifyUserByID.
|
||||
func (mr *MockQueriesMockRecorder) GetNotifyUserByID(ctx, shouldTriggered, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifyUserByID", reflect.TypeOf((*MockQueries)(nil).GetNotifyUserByID), ctx, shouldTriggered, userID)
|
||||
}
|
||||
|
||||
// InstanceByID mocks base method.
|
||||
func (m *MockQueries) InstanceByID(ctx context.Context, id string) (authz.Instance, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InstanceByID", ctx, id)
|
||||
ret0, _ := ret[0].(authz.Instance)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InstanceByID indicates an expected call of InstanceByID.
|
||||
func (mr *MockQueriesMockRecorder) InstanceByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceByID", reflect.TypeOf((*MockQueries)(nil).InstanceByID), ctx, id)
|
||||
}
|
||||
|
||||
// MailTemplateByOrg mocks base method.
|
||||
func (m *MockQueries) MailTemplateByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.MailTemplate, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "MailTemplateByOrg", ctx, orgID, withOwnerRemoved)
|
||||
ret0, _ := ret[0].(*query.MailTemplate)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// MailTemplateByOrg indicates an expected call of MailTemplateByOrg.
|
||||
func (mr *MockQueriesMockRecorder) MailTemplateByOrg(ctx, orgID, withOwnerRemoved any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailTemplateByOrg", reflect.TypeOf((*MockQueries)(nil).MailTemplateByOrg), ctx, orgID, withOwnerRemoved)
|
||||
}
|
||||
|
||||
// NotificationPolicyByOrg mocks base method.
|
||||
func (m *MockQueries) NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string, withOwnerRemoved bool) (*query.NotificationPolicy, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "NotificationPolicyByOrg", ctx, shouldTriggerBulk, orgID, withOwnerRemoved)
|
||||
ret0, _ := ret[0].(*query.NotificationPolicy)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// NotificationPolicyByOrg indicates an expected call of NotificationPolicyByOrg.
|
||||
func (mr *MockQueriesMockRecorder) NotificationPolicyByOrg(ctx, shouldTriggerBulk, orgID, withOwnerRemoved any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationPolicyByOrg", reflect.TypeOf((*MockQueries)(nil).NotificationPolicyByOrg), ctx, shouldTriggerBulk, orgID, withOwnerRemoved)
|
||||
}
|
||||
|
||||
// NotificationProviderByIDAndType mocks base method.
|
||||
func (m *MockQueries) NotificationProviderByIDAndType(ctx context.Context, aggID string, providerType domain.NotificationProviderType) (*query.DebugNotificationProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "NotificationProviderByIDAndType", ctx, aggID, providerType)
|
||||
ret0, _ := ret[0].(*query.DebugNotificationProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// NotificationProviderByIDAndType indicates an expected call of NotificationProviderByIDAndType.
|
||||
func (mr *MockQueriesMockRecorder) NotificationProviderByIDAndType(ctx, aggID, providerType any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationProviderByIDAndType", reflect.TypeOf((*MockQueries)(nil).NotificationProviderByIDAndType), ctx, aggID, providerType)
|
||||
}
|
||||
|
||||
// SMSProviderConfigActive mocks base method.
|
||||
func (m *MockQueries) SMSProviderConfigActive(ctx context.Context, resourceOwner string) (*query.SMSConfig, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SMSProviderConfigActive", ctx, resourceOwner)
|
||||
ret0, _ := ret[0].(*query.SMSConfig)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SMSProviderConfigActive indicates an expected call of SMSProviderConfigActive.
|
||||
func (mr *MockQueriesMockRecorder) SMSProviderConfigActive(ctx, resourceOwner any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMSProviderConfigActive", reflect.TypeOf((*MockQueries)(nil).SMSProviderConfigActive), ctx, resourceOwner)
|
||||
}
|
||||
|
||||
// SMTPConfigActive mocks base method.
|
||||
func (m *MockQueries) SMTPConfigActive(ctx context.Context, resourceOwner string) (*query.SMTPConfig, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SMTPConfigActive", ctx, resourceOwner)
|
||||
ret0, _ := ret[0].(*query.SMTPConfig)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SMTPConfigActive indicates an expected call of SMTPConfigActive.
|
||||
func (mr *MockQueriesMockRecorder) SMTPConfigActive(ctx, resourceOwner any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMTPConfigActive", reflect.TypeOf((*MockQueries)(nil).SMTPConfigActive), ctx, resourceOwner)
|
||||
}
|
||||
|
||||
// SearchInstanceDomains mocks base method.
|
||||
func (m *MockQueries) SearchInstanceDomains(ctx context.Context, queries *query.InstanceDomainSearchQueries) (*query.InstanceDomains, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SearchInstanceDomains", ctx, queries)
|
||||
ret0, _ := ret[0].(*query.InstanceDomains)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SearchInstanceDomains indicates an expected call of SearchInstanceDomains.
|
||||
func (mr *MockQueriesMockRecorder) SearchInstanceDomains(ctx, queries any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchInstanceDomains", reflect.TypeOf((*MockQueries)(nil).SearchInstanceDomains), ctx, queries)
|
||||
}
|
||||
|
||||
// SearchMilestones mocks base method.
|
||||
func (m *MockQueries) SearchMilestones(ctx context.Context, instanceIDs []string, queries *query.MilestonesSearchQueries) (*query.Milestones, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SearchMilestones", ctx, instanceIDs, queries)
|
||||
ret0, _ := ret[0].(*query.Milestones)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SearchMilestones indicates an expected call of SearchMilestones.
|
||||
func (mr *MockQueriesMockRecorder) SearchMilestones(ctx, instanceIDs, queries any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchMilestones", reflect.TypeOf((*MockQueries)(nil).SearchMilestones), ctx, instanceIDs, queries)
|
||||
}
|
||||
|
||||
// SessionByID mocks base method.
|
||||
func (m *MockQueries) SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string, check domain.PermissionCheck) (*query.Session, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SessionByID", ctx, shouldTriggerBulk, id, sessionToken, check)
|
||||
ret0, _ := ret[0].(*query.Session)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SessionByID indicates an expected call of SessionByID.
|
||||
func (mr *MockQueriesMockRecorder) SessionByID(ctx, shouldTriggerBulk, id, sessionToken, check any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SessionByID", reflect.TypeOf((*MockQueries)(nil).SessionByID), ctx, shouldTriggerBulk, id, sessionToken, check)
|
||||
}
|
62
apps/api/internal/notification/handlers/mock/queue.mock.go
Normal file
62
apps/api/internal/notification/handlers/mock/queue.mock.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/zitadel/zitadel/internal/notification/handlers (interfaces: Queue)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package mock -destination ./mock/queue.mock.go github.com/zitadel/zitadel/internal/notification/handlers Queue
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
river "github.com/riverqueue/river"
|
||||
queue "github.com/zitadel/zitadel/internal/queue"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockQueue is a mock of Queue interface.
|
||||
type MockQueue struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockQueueMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockQueueMockRecorder is the mock recorder for MockQueue.
|
||||
type MockQueueMockRecorder struct {
|
||||
mock *MockQueue
|
||||
}
|
||||
|
||||
// NewMockQueue creates a new mock instance.
|
||||
func NewMockQueue(ctrl *gomock.Controller) *MockQueue {
|
||||
mock := &MockQueue{ctrl: ctrl}
|
||||
mock.recorder = &MockQueueMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockQueue) EXPECT() *MockQueueMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Insert mocks base method.
|
||||
func (m *MockQueue) Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx, args}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Insert", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Insert indicates an expected call of Insert.
|
||||
func (mr *MockQueueMockRecorder) Insert(ctx, args any, opts ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{ctx, args}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockQueue)(nil).Insert), varargs...)
|
||||
}
|
186
apps/api/internal/notification/handlers/notification_worker.go
Normal file
186
apps/api/internal/notification/handlers/notification_worker.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
"github.com/zitadel/zitadel/internal/notification/senders"
|
||||
"github.com/zitadel/zitadel/internal/notification/types"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/repository/notification"
|
||||
)
|
||||
|
||||
const (
|
||||
Code = "Code"
|
||||
OTP = "OTP"
|
||||
)
|
||||
|
||||
type NotificationWorker struct {
|
||||
river.WorkerDefaults[*notification.Request]
|
||||
|
||||
commands Commands
|
||||
queries *NotificationQueries
|
||||
channels types.ChannelChains
|
||||
config WorkerConfig
|
||||
now nowFunc
|
||||
}
|
||||
|
||||
// Timeout implements the Timeout-function of [river.Worker].
|
||||
// Maximum time a job can run before the context gets cancelled.
|
||||
func (w *NotificationWorker) Timeout(*river.Job[*notification.Request]) time.Duration {
|
||||
return w.config.TransactionDuration
|
||||
}
|
||||
|
||||
// Work implements [river.Worker].
|
||||
func (w *NotificationWorker) Work(ctx context.Context, job *river.Job[*notification.Request]) error {
|
||||
ctx = ContextWithNotifier(ctx, job.Args.Aggregate)
|
||||
|
||||
// if the notification is too old, we can directly cancel
|
||||
if job.CreatedAt.Add(w.config.MaxTtl).Before(w.now()) {
|
||||
return river.JobCancel(errors.New("notification is too old"))
|
||||
}
|
||||
|
||||
// We do not trigger the projection to reduce load on the database. By the time the notification is processed,
|
||||
// the user should be projected anyway. If not, it will just wait for the next run.
|
||||
// We are aware that the user can change during the time the notification is in the queue.
|
||||
notifyUser, err := w.queries.GetNotifyUserByID(ctx, false, job.Args.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The domain claimed event requires the domain as argument, but lacks the user when creating the request event.
|
||||
// Since we set it into the request arguments, it will be passed into a potential retry event.
|
||||
if job.Args.RequiresPreviousDomain && job.Args.Args != nil && job.Args.Args.Domain == "" {
|
||||
index := strings.LastIndex(notifyUser.LastEmail, "@")
|
||||
job.Args.Args.Domain = notifyUser.LastEmail[index+1:]
|
||||
}
|
||||
|
||||
err = w.sendNotificationQueue(ctx, job.Args, strconv.Itoa(int(job.ID)), notifyUser)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// if the error explicitly specifies, we cancel the notification
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
return river.JobCancel(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type WorkerConfig struct {
|
||||
LegacyEnabled bool
|
||||
Workers uint8
|
||||
TransactionDuration time.Duration
|
||||
MaxTtl time.Duration
|
||||
MaxAttempts uint8
|
||||
}
|
||||
|
||||
// nowFunc makes [time.Now] mockable
|
||||
type nowFunc func() time.Time
|
||||
|
||||
type Sent func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error
|
||||
|
||||
var sentHandlers map[eventstore.EventType]Sent
|
||||
|
||||
func RegisterSentHandler(eventType eventstore.EventType, sent Sent) {
|
||||
if sentHandlers == nil {
|
||||
sentHandlers = make(map[eventstore.EventType]Sent)
|
||||
}
|
||||
sentHandlers[eventType] = sent
|
||||
}
|
||||
|
||||
func NewNotificationWorker(
|
||||
config WorkerConfig,
|
||||
commands Commands,
|
||||
queries *NotificationQueries,
|
||||
channels types.ChannelChains,
|
||||
) *NotificationWorker {
|
||||
return &NotificationWorker{
|
||||
config: config,
|
||||
commands: commands,
|
||||
queries: queries,
|
||||
channels: channels,
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
var _ river.Worker[*notification.Request] = (*NotificationWorker)(nil)
|
||||
|
||||
func (w *NotificationWorker) Register(workers *river.Workers, queues map[string]river.QueueConfig) {
|
||||
river.AddWorker(workers, w)
|
||||
queues[notification.QueueName] = river.QueueConfig{
|
||||
MaxWorkers: int(w.config.Workers),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *NotificationWorker) sendNotificationQueue(ctx context.Context, request *notification.Request, jobID string, notifyUser *query.NotifyUser) error {
|
||||
// check early that a "sent" handler exists, otherwise we can cancel early
|
||||
sentHandler, ok := sentHandlers[request.EventType]
|
||||
if !ok {
|
||||
logging.Errorf(`no "sent" handler registered for %s`, request.EventType)
|
||||
return channels.NewCancelError(fmt.Errorf("no sent handler registered for %s", request.EventType))
|
||||
}
|
||||
|
||||
ctx, err := enrichCtx(ctx, request.TriggeredAtOrigin)
|
||||
if err != nil {
|
||||
return channels.NewCancelError(err)
|
||||
}
|
||||
|
||||
var code string
|
||||
if request.Code != nil {
|
||||
code, err = crypto.DecryptString(request.Code, w.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
colors, err := w.queries.ActiveLabelPolicyByOrg(ctx, request.UserResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
translator, err := w.queries.GetTranslatorWithOrgTexts(ctx, request.UserResourceOwner, request.MessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
generatorInfo := new(senders.CodeGeneratorInfo)
|
||||
var notify types.Notify
|
||||
switch request.NotificationType {
|
||||
case domain.NotificationTypeEmail:
|
||||
template, err := w.queries.MailTemplateByOrg(ctx, notifyUser.ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notify = types.SendEmail(ctx, w.channels, string(template.Template), translator, notifyUser, colors, request.EventType)
|
||||
case domain.NotificationTypeSms:
|
||||
notify = types.SendSMS(ctx, w.channels, translator, notifyUser, colors, request.EventType, request.Aggregate.InstanceID, jobID, generatorInfo)
|
||||
}
|
||||
|
||||
args := request.Args.ToMap()
|
||||
args[Code] = code
|
||||
// existing notifications use `OTP` as argument for the code
|
||||
if request.IsOTP {
|
||||
args[OTP] = code
|
||||
}
|
||||
|
||||
if err = notify(request.URLTemplate, args, request.MessageType, request.UnverifiedNotificationChannel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = sentHandler(authz.WithInstanceID(ctx, request.Aggregate.InstanceID), w.commands, request.Aggregate.ID, request.Aggregate.ResourceOwner, generatorInfo, args)
|
||||
logging.WithFields("instanceID", request.Aggregate.InstanceID, "notification", request.Aggregate.ID).
|
||||
OnError(err).Error("could not set notification event on aggregate")
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,477 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/riverqueue/river/rivertype"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
es_repo_mock "github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/email"
|
||||
channel_mock "github.com/zitadel/zitadel/internal/notification/channels/mock"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/sms"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
|
||||
"github.com/zitadel/zitadel/internal/notification/handlers/mock"
|
||||
"github.com/zitadel/zitadel/internal/notification/messages"
|
||||
"github.com/zitadel/zitadel/internal/notification/senders"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/repository/notification"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
const (
|
||||
notificationID = "notificationID"
|
||||
)
|
||||
|
||||
func Test_userNotifier_reduceNotificationRequested(t *testing.T) {
|
||||
testNow := time.Now
|
||||
testBackOff := func(current time.Duration) time.Duration {
|
||||
return time.Second
|
||||
}
|
||||
sendError := errors.New("send error")
|
||||
tests := []struct {
|
||||
name string
|
||||
test func(*gomock.Controller, *mock.MockQueries, *mock.MockCommands) (fieldsWorker, argsWorker, wantWorker)
|
||||
}{
|
||||
{
|
||||
name: "too old",
|
||||
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) {
|
||||
codeAlg, code := cryptoValue(t, ctrl, "testcode")
|
||||
return fieldsWorker{
|
||||
queries: queries,
|
||||
commands: commands,
|
||||
es: eventstore.NewEventstore(&eventstore.Config{
|
||||
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
||||
}),
|
||||
userDataCrypto: codeAlg,
|
||||
now: testNow,
|
||||
},
|
||||
argsWorker{
|
||||
job: &river.Job[*notification.Request]{
|
||||
JobRow: &rivertype.JobRow{
|
||||
CreatedAt: time.Now().Add(-1 * time.Hour),
|
||||
},
|
||||
Args: ¬ification.Request{
|
||||
Aggregate: &eventstore.Aggregate{
|
||||
InstanceID: instanceID,
|
||||
ID: notificationID,
|
||||
ResourceOwner: instanceID,
|
||||
},
|
||||
UserID: userID,
|
||||
UserResourceOwner: orgID,
|
||||
TriggeredAtOrigin: eventOrigin,
|
||||
EventType: user.HumanInviteCodeAddedType,
|
||||
MessageType: domain.InviteUserMessageType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID),
|
||||
CodeExpiry: 1 * time.Hour,
|
||||
Code: code,
|
||||
UnverifiedNotificationChannel: true,
|
||||
IsOTP: false,
|
||||
RequiresPreviousDomain: false,
|
||||
Args: &domain.NotificationArguments{
|
||||
ApplicationName: "APP",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantWorker{
|
||||
err: func(tt assert.TestingT, err error, i ...interface{}) bool {
|
||||
return errors.Is(err, new(river.JobCancelError))
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "send ok (email)",
|
||||
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) {
|
||||
givenTemplate := "{{.LogoURL}}"
|
||||
expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL)
|
||||
w.message = &messages.Email{
|
||||
Recipients: []string{lastEmail},
|
||||
Subject: "Invitation to APP",
|
||||
Content: expectContent,
|
||||
TriggeringEventType: user.HumanInviteCodeAddedType,
|
||||
}
|
||||
codeAlg, code := cryptoValue(t, ctrl, "testcode")
|
||||
expectTemplateWithNotifyUserQueries(queries, givenTemplate)
|
||||
commands.EXPECT().InviteCodeSent(gomock.Any(), orgID, userID).Return(nil)
|
||||
return fieldsWorker{
|
||||
queries: queries,
|
||||
commands: commands,
|
||||
es: eventstore.NewEventstore(&eventstore.Config{
|
||||
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
||||
}),
|
||||
userDataCrypto: codeAlg,
|
||||
now: testNow,
|
||||
},
|
||||
argsWorker{
|
||||
job: &river.Job[*notification.Request]{
|
||||
JobRow: &rivertype.JobRow{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
Args: ¬ification.Request{
|
||||
Aggregate: &eventstore.Aggregate{
|
||||
InstanceID: instanceID,
|
||||
ID: userID,
|
||||
ResourceOwner: orgID,
|
||||
},
|
||||
UserID: userID,
|
||||
UserResourceOwner: orgID,
|
||||
TriggeredAtOrigin: eventOrigin,
|
||||
EventType: user.HumanInviteCodeAddedType,
|
||||
MessageType: domain.InviteUserMessageType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID),
|
||||
CodeExpiry: 1 * time.Hour,
|
||||
Code: code,
|
||||
UnverifiedNotificationChannel: true,
|
||||
IsOTP: false,
|
||||
RequiresPreviousDomain: false,
|
||||
Args: &domain.NotificationArguments{
|
||||
ApplicationName: "APP",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
w
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "send ok (sms with external provider)",
|
||||
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) {
|
||||
expiry := 0 * time.Hour
|
||||
testCode := ""
|
||||
expectContent := fmt.Sprintf(`%[1]s is your one-time password for %[2]s. Use it within the next %[3]s.
|
||||
@%[2]s #%[1]s`, testCode, eventOriginDomain, expiry)
|
||||
w.messageSMS = &messages.SMS{
|
||||
SenderPhoneNumber: "senderNumber",
|
||||
RecipientPhoneNumber: verifiedPhone,
|
||||
Content: expectContent,
|
||||
TriggeringEventType: session.OTPSMSChallengedType,
|
||||
InstanceID: instanceID,
|
||||
JobID: "1",
|
||||
UserID: userID,
|
||||
}
|
||||
codeAlg, code := cryptoValue(t, ctrl, testCode)
|
||||
expectTemplateWithNotifyUserQueriesSMS(queries)
|
||||
commands.EXPECT().OTPSMSSent(gomock.Any(), sessionID, instanceID, &senders.CodeGeneratorInfo{
|
||||
ID: smsProviderID,
|
||||
VerificationID: verificationID,
|
||||
}).Return(nil)
|
||||
return fieldsWorker{
|
||||
queries: queries,
|
||||
commands: commands,
|
||||
es: eventstore.NewEventstore(&eventstore.Config{
|
||||
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
||||
}),
|
||||
userDataCrypto: codeAlg,
|
||||
now: testNow,
|
||||
},
|
||||
argsWorker{
|
||||
job: &river.Job[*notification.Request]{
|
||||
JobRow: &rivertype.JobRow{
|
||||
CreatedAt: time.Now(),
|
||||
ID: 1,
|
||||
},
|
||||
Args: ¬ification.Request{
|
||||
Aggregate: &eventstore.Aggregate{
|
||||
InstanceID: instanceID,
|
||||
ID: sessionID,
|
||||
ResourceOwner: instanceID,
|
||||
},
|
||||
UserID: userID,
|
||||
UserResourceOwner: orgID,
|
||||
TriggeredAtOrigin: eventOrigin,
|
||||
EventType: session.OTPSMSChallengedType,
|
||||
MessageType: domain.VerifySMSOTPMessageType,
|
||||
NotificationType: domain.NotificationTypeSms,
|
||||
URLTemplate: "",
|
||||
CodeExpiry: expiry,
|
||||
Code: code,
|
||||
UnverifiedNotificationChannel: false,
|
||||
IsOTP: true,
|
||||
RequiresPreviousDomain: false,
|
||||
Args: &domain.NotificationArguments{
|
||||
Origin: eventOrigin,
|
||||
Domain: eventOriginDomain,
|
||||
Expiry: expiry,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, w
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "previous domain",
|
||||
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) {
|
||||
givenTemplate := "{{.LogoURL}}"
|
||||
expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL)
|
||||
w.message = &messages.Email{
|
||||
Recipients: []string{verifiedEmail},
|
||||
Subject: "Domain has been claimed",
|
||||
Content: expectContent,
|
||||
TriggeringEventType: user.UserDomainClaimedType,
|
||||
}
|
||||
expectTemplateWithNotifyUserQueries(queries, givenTemplate)
|
||||
commands.EXPECT().UserDomainClaimedSent(gomock.Any(), orgID, userID).Return(nil)
|
||||
return fieldsWorker{
|
||||
queries: queries,
|
||||
commands: commands,
|
||||
es: eventstore.NewEventstore(&eventstore.Config{
|
||||
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
||||
}),
|
||||
userDataCrypto: nil,
|
||||
now: testNow,
|
||||
},
|
||||
argsWorker{
|
||||
job: &river.Job[*notification.Request]{
|
||||
JobRow: &rivertype.JobRow{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
Args: ¬ification.Request{
|
||||
Aggregate: &eventstore.Aggregate{
|
||||
InstanceID: instanceID,
|
||||
ID: userID,
|
||||
ResourceOwner: orgID,
|
||||
},
|
||||
UserID: userID,
|
||||
UserResourceOwner: orgID,
|
||||
TriggeredAtOrigin: eventOrigin,
|
||||
EventType: user.UserDomainClaimedType,
|
||||
MessageType: domain.DomainClaimedMessageType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
URLTemplate: login.LoginLink(eventOrigin, orgID),
|
||||
CodeExpiry: 0,
|
||||
Code: nil,
|
||||
UnverifiedNotificationChannel: false,
|
||||
IsOTP: false,
|
||||
RequiresPreviousDomain: true,
|
||||
Args: &domain.NotificationArguments{
|
||||
TempUsername: "tempUsername",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, w
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "send failed, retry",
|
||||
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) {
|
||||
givenTemplate := "{{.LogoURL}}"
|
||||
expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL)
|
||||
w.message = &messages.Email{
|
||||
Recipients: []string{lastEmail},
|
||||
Subject: "Invitation to APP",
|
||||
Content: expectContent,
|
||||
TriggeringEventType: user.HumanInviteCodeAddedType,
|
||||
}
|
||||
w.sendError = sendError
|
||||
w.err = func(tt assert.TestingT, err error, i ...interface{}) bool {
|
||||
return errors.Is(err, sendError)
|
||||
}
|
||||
codeAlg, code := cryptoValue(t, ctrl, "testcode")
|
||||
expectTemplateWithNotifyUserQueries(queries, givenTemplate)
|
||||
return fieldsWorker{
|
||||
queries: queries,
|
||||
commands: commands,
|
||||
es: eventstore.NewEventstore(&eventstore.Config{
|
||||
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
||||
}),
|
||||
userDataCrypto: codeAlg,
|
||||
now: testNow,
|
||||
backOff: testBackOff,
|
||||
},
|
||||
argsWorker{
|
||||
job: &river.Job[*notification.Request]{
|
||||
JobRow: &rivertype.JobRow{
|
||||
ID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
Args: ¬ification.Request{
|
||||
Aggregate: &eventstore.Aggregate{
|
||||
InstanceID: instanceID,
|
||||
ID: notificationID,
|
||||
ResourceOwner: instanceID,
|
||||
},
|
||||
UserID: userID,
|
||||
UserResourceOwner: orgID,
|
||||
TriggeredAtOrigin: eventOrigin,
|
||||
EventType: user.HumanInviteCodeAddedType,
|
||||
MessageType: domain.InviteUserMessageType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID),
|
||||
CodeExpiry: 1 * time.Hour,
|
||||
Code: code,
|
||||
UnverifiedNotificationChannel: true,
|
||||
IsOTP: false,
|
||||
RequiresPreviousDomain: false,
|
||||
Args: &domain.NotificationArguments{
|
||||
ApplicationName: "APP",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
w
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "send failed (max attempts), cancel",
|
||||
test: func(ctrl *gomock.Controller, queries *mock.MockQueries, commands *mock.MockCommands) (f fieldsWorker, a argsWorker, w wantWorker) {
|
||||
givenTemplate := "{{.LogoURL}}"
|
||||
expectContent := fmt.Sprintf("%s%s/%s/%s", eventOrigin, assetsPath, policyID, logoURL)
|
||||
w.message = &messages.Email{
|
||||
Recipients: []string{lastEmail},
|
||||
Subject: "Invitation to APP",
|
||||
Content: expectContent,
|
||||
TriggeringEventType: user.HumanInviteCodeAddedType,
|
||||
}
|
||||
w.sendError = sendError
|
||||
w.err = func(tt assert.TestingT, err error, i ...interface{}) bool {
|
||||
return err != nil
|
||||
}
|
||||
|
||||
codeAlg, code := cryptoValue(t, ctrl, "testcode")
|
||||
expectTemplateWithNotifyUserQueries(queries, givenTemplate)
|
||||
return fieldsWorker{
|
||||
queries: queries,
|
||||
commands: commands,
|
||||
es: eventstore.NewEventstore(&eventstore.Config{
|
||||
Querier: es_repo_mock.NewRepo(t).MockQuerier,
|
||||
}),
|
||||
userDataCrypto: codeAlg,
|
||||
now: testNow,
|
||||
backOff: testBackOff,
|
||||
},
|
||||
argsWorker{
|
||||
job: &river.Job[*notification.Request]{
|
||||
JobRow: &rivertype.JobRow{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
Args: ¬ification.Request{
|
||||
Aggregate: &eventstore.Aggregate{
|
||||
InstanceID: instanceID,
|
||||
ID: userID,
|
||||
ResourceOwner: orgID,
|
||||
},
|
||||
UserID: userID,
|
||||
UserResourceOwner: orgID,
|
||||
TriggeredAtOrigin: eventOrigin,
|
||||
EventType: user.HumanInviteCodeAddedType,
|
||||
MessageType: domain.InviteUserMessageType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
URLTemplate: fmt.Sprintf("%s/ui/login/user/invite?userID=%s&loginname={{.LoginName}}&code={{.Code}}&orgID=%s&authRequestID=%s", eventOrigin, userID, orgID, authRequestID),
|
||||
CodeExpiry: 1 * time.Hour,
|
||||
Code: code,
|
||||
UnverifiedNotificationChannel: true,
|
||||
IsOTP: false,
|
||||
RequiresPreviousDomain: false,
|
||||
Args: &domain.NotificationArguments{
|
||||
ApplicationName: "APP",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
w
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
queries := mock.NewMockQueries(ctrl)
|
||||
commands := mock.NewMockCommands(ctrl)
|
||||
f, a, w := tt.test(ctrl, queries, commands)
|
||||
err := newNotificationWorker(t, ctrl, queries, f, w).Work(
|
||||
authz.WithInstanceID(context.Background(), instanceID),
|
||||
a.job,
|
||||
)
|
||||
if w.err != nil {
|
||||
w.err(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newNotificationWorker(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fieldsWorker, w wantWorker) *NotificationWorker {
|
||||
queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil)
|
||||
smtpAlg, _ := cryptoValue(t, ctrl, "smtppw")
|
||||
channel := channel_mock.NewMockNotificationChannel(ctrl)
|
||||
if w.message != nil {
|
||||
channel.EXPECT().HandleMessage(w.message).Return(w.sendError)
|
||||
}
|
||||
if w.messageSMS != nil {
|
||||
channel.EXPECT().HandleMessage(w.messageSMS).DoAndReturn(func(message *messages.SMS) error {
|
||||
message.VerificationID = gu.Ptr(verificationID)
|
||||
return w.sendError
|
||||
})
|
||||
}
|
||||
return &NotificationWorker{
|
||||
commands: f.commands,
|
||||
queries: NewNotificationQueries(
|
||||
f.queries,
|
||||
f.es,
|
||||
externalDomain,
|
||||
externalPort,
|
||||
externalSecure,
|
||||
"",
|
||||
f.userDataCrypto,
|
||||
smtpAlg,
|
||||
f.SMSTokenCrypto,
|
||||
),
|
||||
channels: ¬ificationChannels{
|
||||
Chain: *senders.ChainChannels(channel),
|
||||
EmailConfig: &email.Config{
|
||||
ProviderConfig: &email.Provider{
|
||||
ID: "emailProviderID",
|
||||
Description: "description",
|
||||
},
|
||||
SMTPConfig: &smtp.Config{
|
||||
SMTP: smtp.SMTP{
|
||||
Host: "host",
|
||||
User: "user",
|
||||
Password: "password",
|
||||
},
|
||||
Tls: true,
|
||||
From: "from",
|
||||
FromName: "fromName",
|
||||
ReplyToAddress: "replyToAddress",
|
||||
},
|
||||
WebhookConfig: nil,
|
||||
},
|
||||
SMSConfig: &sms.Config{
|
||||
ProviderConfig: &sms.Provider{
|
||||
ID: "smsProviderID",
|
||||
Description: "description",
|
||||
},
|
||||
TwilioConfig: &twilio.Config{
|
||||
SID: "sid",
|
||||
Token: "token",
|
||||
SenderNumber: "senderNumber",
|
||||
VerifyServiceSID: "verifyServiceSID",
|
||||
},
|
||||
},
|
||||
},
|
||||
config: WorkerConfig{
|
||||
Workers: 1,
|
||||
TransactionDuration: 5 * time.Second,
|
||||
MaxTtl: 5 * time.Minute,
|
||||
},
|
||||
now: f.now,
|
||||
}
|
||||
}
|
61
apps/api/internal/notification/handlers/origin.go
Normal file
61
apps/api/internal/notification/handlers/origin.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type OriginEvent interface {
|
||||
eventstore.Event
|
||||
TriggerOrigin() string
|
||||
}
|
||||
|
||||
func (n *NotificationQueries) Origin(ctx context.Context, e eventstore.Event) (context.Context, error) {
|
||||
var origin string
|
||||
originEvent, ok := e.(OriginEvent)
|
||||
if !ok {
|
||||
logging.Errorf("event of type %T doesn't implement OriginEvent", e)
|
||||
} else {
|
||||
origin = originEvent.TriggerOrigin()
|
||||
}
|
||||
if origin != "" {
|
||||
return enrichCtx(ctx, origin)
|
||||
}
|
||||
primary, err := query.NewInstanceDomainPrimarySearchQuery(true)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
domains, err := n.SearchInstanceDomains(ctx, &query.InstanceDomainSearchQueries{
|
||||
Queries: []query.SearchQuery{primary},
|
||||
})
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
if len(domains.Domains) < 1 {
|
||||
return ctx, zerrors.ThrowInternal(nil, "NOTIF-Ef3r1", "Errors.Notification.NoDomain")
|
||||
}
|
||||
return enrichCtx(
|
||||
ctx,
|
||||
http_utils.BuildHTTP(domains.Domains[0].Domain, n.externalPort, n.externalSecure),
|
||||
)
|
||||
}
|
||||
|
||||
func enrichCtx(ctx context.Context, origin string) (context.Context, error) {
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx = http_utils.WithDomainContext(ctx, &http_utils.DomainCtx{
|
||||
InstanceHost: u.Host,
|
||||
PublicHost: u.Host,
|
||||
Protocol: u.Scheme,
|
||||
})
|
||||
return ctx, nil
|
||||
}
|
70
apps/api/internal/notification/handlers/queries.go
Normal file
70
apps/api/internal/notification/handlers/queries.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type Queries interface {
|
||||
ActiveLabelPolicyByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.LabelPolicy, error)
|
||||
MailTemplateByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.MailTemplate, error)
|
||||
GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string) (*query.NotifyUser, error)
|
||||
CustomTextListByTemplate(ctx context.Context, aggregateID, template string, withOwnerRemoved bool) (*query.CustomTexts, error)
|
||||
SearchInstanceDomains(ctx context.Context, queries *query.InstanceDomainSearchQueries) (*query.InstanceDomains, error)
|
||||
SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string, check domain.PermissionCheck) (*query.Session, error)
|
||||
NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string, withOwnerRemoved bool) (*query.NotificationPolicy, error)
|
||||
SearchMilestones(ctx context.Context, instanceIDs []string, queries *query.MilestonesSearchQueries) (*query.Milestones, error)
|
||||
NotificationProviderByIDAndType(ctx context.Context, aggID string, providerType domain.NotificationProviderType) (*query.DebugNotificationProvider, error)
|
||||
SMSProviderConfigActive(ctx context.Context, resourceOwner string) (config *query.SMSConfig, err error)
|
||||
SMTPConfigActive(ctx context.Context, resourceOwner string) (*query.SMTPConfig, error)
|
||||
GetDefaultLanguage(ctx context.Context) language.Tag
|
||||
GetInstanceRestrictions(ctx context.Context) (restrictions query.Restrictions, err error)
|
||||
InstanceByID(ctx context.Context, id string) (instance authz.Instance, err error)
|
||||
GetActiveSigningWebKey(ctx context.Context) (*jose.JSONWebKey, error)
|
||||
|
||||
ActiveInstances() []string
|
||||
}
|
||||
|
||||
type NotificationQueries struct {
|
||||
Queries
|
||||
es *eventstore.Eventstore
|
||||
externalDomain string
|
||||
externalPort uint16
|
||||
externalSecure bool
|
||||
fileSystemPath string
|
||||
UserDataCrypto crypto.EncryptionAlgorithm
|
||||
SMTPPasswordCrypto crypto.EncryptionAlgorithm
|
||||
SMSTokenCrypto crypto.EncryptionAlgorithm
|
||||
}
|
||||
|
||||
func NewNotificationQueries(
|
||||
baseQueries Queries,
|
||||
es *eventstore.Eventstore,
|
||||
externalDomain string,
|
||||
externalPort uint16,
|
||||
externalSecure bool,
|
||||
fileSystemPath string,
|
||||
userDataCrypto crypto.EncryptionAlgorithm,
|
||||
smtpPasswordCrypto crypto.EncryptionAlgorithm,
|
||||
smsTokenCrypto crypto.EncryptionAlgorithm,
|
||||
) *NotificationQueries {
|
||||
return &NotificationQueries{
|
||||
Queries: baseQueries,
|
||||
es: es,
|
||||
externalDomain: externalDomain,
|
||||
externalPort: externalPort,
|
||||
externalSecure: externalSecure,
|
||||
fileSystemPath: fileSystemPath,
|
||||
UserDataCrypto: userDataCrypto,
|
||||
SMTPPasswordCrypto: smtpPasswordCrypto,
|
||||
SMSTokenCrypto: smsTokenCrypto,
|
||||
}
|
||||
}
|
13
apps/api/internal/notification/handlers/queue.go
Normal file
13
apps/api/internal/notification/handlers/queue.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/riverqueue/river"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/queue"
|
||||
)
|
||||
|
||||
type Queue interface {
|
||||
Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error
|
||||
}
|
80
apps/api/internal/notification/handlers/quota_notifier.go
Normal file
80
apps/api/internal/notification/handlers/quota_notifier.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
|
||||
_ "github.com/zitadel/zitadel/internal/notification/statik"
|
||||
"github.com/zitadel/zitadel/internal/notification/types"
|
||||
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
QuotaNotificationsProjectionTable = "projections.notifications_quota"
|
||||
)
|
||||
|
||||
type quotaNotifier struct {
|
||||
commands *command.Commands
|
||||
queries *NotificationQueries
|
||||
channels types.ChannelChains
|
||||
}
|
||||
|
||||
func NewQuotaNotifier(
|
||||
ctx context.Context,
|
||||
config handler.Config,
|
||||
commands *command.Commands,
|
||||
queries *NotificationQueries,
|
||||
channels types.ChannelChains,
|
||||
) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, "aNotifier{
|
||||
commands: commands,
|
||||
queries: queries,
|
||||
channels: channels,
|
||||
})
|
||||
}
|
||||
|
||||
func (*quotaNotifier) Name() string {
|
||||
return QuotaNotificationsProjectionTable
|
||||
}
|
||||
|
||||
func (u *quotaNotifier) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: quota.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: quota.NotificationDueEventType,
|
||||
Reduce: u.reduceNotificationDue,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (u *quotaNotifier) reduceNotificationDue(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*quota.NotificationDueEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-DLxdE", "reduce.wrong.event.type %s", quota.NotificationDueEventType)
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, map[string]interface{}{"dueEventID": e.ID}, quota.NotifiedEventType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
err = types.SendJSON(ctx, webhook.Config{CallURL: e.CallURL, Method: http.MethodPost}, u.channels, e, e.Type()).WithoutTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return u.commands.UsageNotificationSent(ctx, e)
|
||||
}), nil
|
||||
}
|
111
apps/api/internal/notification/handlers/telemetry_pusher.go
Normal file
111
apps/api/internal/notification/handlers/telemetry_pusher.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
|
||||
_ "github.com/zitadel/zitadel/internal/notification/statik"
|
||||
"github.com/zitadel/zitadel/internal/notification/types"
|
||||
"github.com/zitadel/zitadel/internal/repository/milestone"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
TelemetryProjectionTable = "projections.telemetry"
|
||||
)
|
||||
|
||||
type TelemetryPusherConfig struct {
|
||||
Enabled bool
|
||||
Endpoints []string
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
type telemetryPusher struct {
|
||||
cfg TelemetryPusherConfig
|
||||
commands *command.Commands
|
||||
queries *NotificationQueries
|
||||
channels types.ChannelChains
|
||||
}
|
||||
|
||||
func NewTelemetryPusher(
|
||||
ctx context.Context,
|
||||
telemetryCfg TelemetryPusherConfig,
|
||||
handlerCfg handler.Config,
|
||||
commands *command.Commands,
|
||||
queries *NotificationQueries,
|
||||
channels types.ChannelChains,
|
||||
) *handler.Handler {
|
||||
pusher := &telemetryPusher{
|
||||
cfg: telemetryCfg,
|
||||
commands: commands,
|
||||
queries: queries,
|
||||
channels: channels,
|
||||
}
|
||||
return handler.NewHandler(
|
||||
ctx,
|
||||
&handlerCfg,
|
||||
pusher,
|
||||
)
|
||||
}
|
||||
|
||||
func (u *telemetryPusher) Name() string {
|
||||
return TelemetryProjectionTable
|
||||
}
|
||||
|
||||
func (t *telemetryPusher) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{{
|
||||
Aggregate: milestone.AggregateType,
|
||||
EventReducers: []handler.EventReducer{{
|
||||
Event: milestone.ReachedEventType,
|
||||
Reduce: t.pushMilestones,
|
||||
}},
|
||||
}}
|
||||
}
|
||||
|
||||
func (t *telemetryPusher) pushMilestones(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*milestone.ReachedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-lDTs5", "reduce.wrong.event.type %s", event.Type())
|
||||
}
|
||||
return handler.NewStatement(event, func(ctx context.Context, _ handler.Executer, _ string) error {
|
||||
// Do not push the milestone again if this was a migration event.
|
||||
if e.ReachedDate != nil {
|
||||
return nil
|
||||
}
|
||||
return t.pushMilestone(ctx, e)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (t *telemetryPusher) pushMilestone(ctx context.Context, e *milestone.ReachedEvent) error {
|
||||
for _, endpoint := range t.cfg.Endpoints {
|
||||
if err := types.SendJSON(
|
||||
ctx,
|
||||
webhook.Config{
|
||||
CallURL: endpoint,
|
||||
Method: http.MethodPost,
|
||||
Headers: t.cfg.Headers,
|
||||
},
|
||||
t.channels,
|
||||
&struct {
|
||||
InstanceID string `json:"instanceId"`
|
||||
ExternalDomain string `json:"externalDomain"`
|
||||
Type milestone.Type `json:"type"`
|
||||
ReachedDate time.Time `json:"reached"`
|
||||
}{
|
||||
InstanceID: e.Agg.InstanceID,
|
||||
ExternalDomain: t.queries.externalDomain,
|
||||
Type: e.MilestoneType,
|
||||
ReachedDate: e.GetReachedDate(),
|
||||
},
|
||||
e.EventType,
|
||||
).WithoutTemplate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return t.commands.MilestonePushed(ctx, e.Agg.InstanceID, e.MilestoneType, t.cfg.Endpoints)
|
||||
}
|
43
apps/api/internal/notification/handlers/translator.go
Normal file
43
apps/api/internal/notification/handlers/translator.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
)
|
||||
|
||||
func (n *NotificationQueries) GetTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) {
|
||||
restrictions, err := n.Queries.GetInstanceRestrictions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := i18n.NewNotificationTranslator(n.GetDefaultLanguage(ctx), restrictions.AllowedLanguages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allCustomTexts, err := n.CustomTextListByTemplate(ctx, authz.GetInstance(ctx).InstanceID(), textType, false)
|
||||
if err != nil {
|
||||
return translator, nil
|
||||
}
|
||||
customTexts, err := n.CustomTextListByTemplate(ctx, orgID, textType, false)
|
||||
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
|
||||
}
|
828
apps/api/internal/notification/handlers/user_notifier.go
Normal file
828
apps/api/internal/notification/handlers/user_notifier.go
Normal file
@@ -0,0 +1,828 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/console"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/notification/senders"
|
||||
"github.com/zitadel/zitadel/internal/notification/types"
|
||||
"github.com/zitadel/zitadel/internal/queue"
|
||||
"github.com/zitadel/zitadel/internal/repository/notification"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterSentHandler(user.HumanInitialCodeAddedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, _ *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.HumanInitCodeSent(ctx, orgID, id)
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(user.HumanEmailCodeAddedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.HumanEmailVerificationCodeSent(ctx, orgID, id)
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(user.HumanPasswordCodeAddedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.PasswordCodeSent(ctx, orgID, id, generatorInfo)
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(user.HumanOTPSMSCodeAddedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.HumanOTPSMSCodeSent(ctx, id, orgID, generatorInfo)
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(session.OTPSMSChallengedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.OTPSMSSent(ctx, id, orgID, generatorInfo)
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(user.HumanOTPEmailCodeAddedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.HumanOTPEmailCodeSent(ctx, id, orgID)
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(session.OTPEmailChallengedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.OTPEmailSent(ctx, id, orgID)
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(user.UserDomainClaimedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.UserDomainClaimedSent(ctx, orgID, id)
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(user.HumanPasswordlessInitCodeRequestedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.HumanPasswordlessInitCodeSent(ctx, id, orgID, args["CodeID"].(string))
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(user.HumanPasswordChangedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.PasswordChangeSent(ctx, orgID, id)
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(user.HumanPhoneCodeAddedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, generatorInfo *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.HumanPhoneVerificationCodeSent(ctx, orgID, id, generatorInfo)
|
||||
},
|
||||
)
|
||||
RegisterSentHandler(user.HumanInviteCodeAddedType,
|
||||
func(ctx context.Context, commands Commands, id, orgID string, _ *senders.CodeGeneratorInfo, args map[string]any) error {
|
||||
return commands.InviteCodeSent(ctx, orgID, id)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const (
|
||||
UserNotificationsProjectionTable = "projections.notifications"
|
||||
)
|
||||
|
||||
type userNotifier struct {
|
||||
queries *NotificationQueries
|
||||
otpEmailTmpl string
|
||||
|
||||
queue Queue
|
||||
maxAttempts uint8
|
||||
}
|
||||
|
||||
func NewUserNotifier(
|
||||
ctx context.Context,
|
||||
config handler.Config,
|
||||
commands Commands,
|
||||
queries *NotificationQueries,
|
||||
channels types.ChannelChains,
|
||||
otpEmailTmpl string,
|
||||
workerConfig WorkerConfig,
|
||||
queue Queue,
|
||||
) *handler.Handler {
|
||||
if workerConfig.LegacyEnabled {
|
||||
return NewUserNotifierLegacy(ctx, config, commands, queries, channels, otpEmailTmpl)
|
||||
}
|
||||
return handler.NewHandler(ctx, &config, &userNotifier{
|
||||
queries: queries,
|
||||
otpEmailTmpl: otpEmailTmpl,
|
||||
queue: queue,
|
||||
maxAttempts: workerConfig.MaxAttempts,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *userNotifier) Name() string {
|
||||
return UserNotificationsProjectionTable
|
||||
}
|
||||
|
||||
func (u *userNotifier) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: user.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: user.UserV1InitialCodeAddedType,
|
||||
Reduce: u.reduceInitCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanInitialCodeAddedType,
|
||||
Reduce: u.reduceInitCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.UserV1EmailCodeAddedType,
|
||||
Reduce: u.reduceEmailCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanEmailCodeAddedType,
|
||||
Reduce: u.reduceEmailCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.UserV1PasswordCodeAddedType,
|
||||
Reduce: u.reducePasswordCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPasswordCodeAddedType,
|
||||
Reduce: u.reducePasswordCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.UserDomainClaimedType,
|
||||
Reduce: u.reduceDomainClaimed,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPasswordlessInitCodeRequestedType,
|
||||
Reduce: u.reducePasswordlessCodeRequested,
|
||||
},
|
||||
{
|
||||
Event: user.UserV1PhoneCodeAddedType,
|
||||
Reduce: u.reducePhoneCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPhoneCodeAddedType,
|
||||
Reduce: u.reducePhoneCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPasswordChangedType,
|
||||
Reduce: u.reducePasswordChanged,
|
||||
},
|
||||
{
|
||||
Event: user.HumanOTPSMSCodeAddedType,
|
||||
Reduce: u.reduceOTPSMSCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanOTPEmailCodeAddedType,
|
||||
Reduce: u.reduceOTPEmailCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanInviteCodeAddedType,
|
||||
Reduce: u.reduceInviteCodeAdded,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Aggregate: session.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: session.OTPSMSChallengedType,
|
||||
Reduce: u.reduceSessionOTPSMSChallenged,
|
||||
},
|
||||
{
|
||||
Event: session.OTPEmailChallengedType,
|
||||
Reduce: u.reduceSessionOTPEmailChallenged,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceInitCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanInitialCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-EFe2f", "reduce.wrong.event.type %s", user.HumanInitialCodeAddedType)
|
||||
}
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1InitialCodeAddedType, user.UserV1InitialCodeSentType,
|
||||
user.HumanInitialCodeAddedType, user.HumanInitialCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
origin := http_util.DomainContext(ctx).Origin()
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: e.Aggregate().ID,
|
||||
UserResourceOwner: e.Aggregate().ResourceOwner,
|
||||
TriggeredAtOrigin: origin,
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
MessageType: domain.InitCodeMessageType,
|
||||
Code: e.Code,
|
||||
CodeExpiry: e.Expiry,
|
||||
IsOTP: false,
|
||||
UnverifiedNotificationChannel: true,
|
||||
URLTemplate: login.InitUserLinkTemplate(origin, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.AuthRequestID),
|
||||
Args: &domain.NotificationArguments{
|
||||
AuthRequestID: e.AuthRequestID,
|
||||
},
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanEmailCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType)
|
||||
}
|
||||
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType,
|
||||
user.HumanEmailCodeAddedType, user.HumanEmailCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
origin := http_util.DomainContext(ctx).Origin()
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: e.Aggregate().ID,
|
||||
UserResourceOwner: e.Aggregate().ResourceOwner,
|
||||
TriggeredAtOrigin: origin,
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
MessageType: domain.VerifyEmailMessageType,
|
||||
Code: e.Code,
|
||||
CodeExpiry: e.Expiry,
|
||||
IsOTP: false,
|
||||
UnverifiedNotificationChannel: true,
|
||||
URLTemplate: u.emailCodeTemplate(origin, e),
|
||||
Args: &domain.NotificationArguments{
|
||||
AuthRequestID: e.AuthRequestID,
|
||||
},
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) emailCodeTemplate(origin string, e *user.HumanEmailCodeAddedEvent) string {
|
||||
if e.URLTemplate != "" {
|
||||
return e.URLTemplate
|
||||
}
|
||||
return login.MailVerificationLinkTemplate(origin, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.AuthRequestID)
|
||||
}
|
||||
|
||||
func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPasswordCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Eeg3s", "reduce.wrong.event.type %s", user.HumanPasswordCodeAddedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1PasswordCodeAddedType, user.UserV1PasswordCodeSentType,
|
||||
user.HumanPasswordCodeAddedType, user.HumanPasswordCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
origin := http_util.DomainContext(ctx).Origin()
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: e.Aggregate().ID,
|
||||
UserResourceOwner: e.Aggregate().ResourceOwner,
|
||||
TriggeredAtOrigin: origin,
|
||||
EventType: e.EventType,
|
||||
NotificationType: e.NotificationType,
|
||||
MessageType: domain.PasswordResetMessageType,
|
||||
Code: e.Code,
|
||||
CodeExpiry: e.Expiry,
|
||||
IsOTP: false,
|
||||
UnverifiedNotificationChannel: true,
|
||||
URLTemplate: u.passwordCodeTemplate(origin, e),
|
||||
Args: &domain.NotificationArguments{
|
||||
AuthRequestID: e.AuthRequestID,
|
||||
},
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) passwordCodeTemplate(origin string, e *user.HumanPasswordCodeAddedEvent) string {
|
||||
if e.URLTemplate != "" {
|
||||
return e.URLTemplate
|
||||
}
|
||||
return login.InitPasswordLinkTemplate(origin, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.AuthRequestID)
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceOTPSMSCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanOTPSMSCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ASF3g", "reduce.wrong.event.type %s", user.HumanOTPSMSCodeAddedType)
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.HumanOTPSMSCodeAddedType,
|
||||
user.HumanOTPSMSCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: e.Aggregate().ID,
|
||||
UserResourceOwner: e.Aggregate().ResourceOwner,
|
||||
TriggeredAtOrigin: http_util.DomainContext(ctx).Origin(),
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeSms,
|
||||
MessageType: domain.VerifySMSOTPMessageType,
|
||||
Code: e.Code,
|
||||
CodeExpiry: e.Expiry,
|
||||
IsOTP: true,
|
||||
Args: otpArgs(ctx, e.Expiry),
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceSessionOTPSMSChallenged(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.OTPSMSChallengedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Sk32L", "reduce.wrong.event.type %s", session.OTPSMSChallengedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
session.OTPSMSChallengedType,
|
||||
session.OTPSMSSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := otpArgs(ctx, e.Expiry)
|
||||
args.SessionID = e.Aggregate().ID
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: s.UserFactor.UserID,
|
||||
UserResourceOwner: s.UserFactor.ResourceOwner,
|
||||
TriggeredAtOrigin: http_util.DomainContext(ctx).Origin(),
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeSms,
|
||||
MessageType: domain.VerifySMSOTPMessageType,
|
||||
Code: e.Code,
|
||||
CodeExpiry: e.Expiry,
|
||||
IsOTP: true,
|
||||
Args: args,
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceOTPEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanOTPEmailCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-JL3hw", "reduce.wrong.event.type %s", user.HumanOTPEmailCodeAddedType)
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.HumanOTPEmailCodeAddedType,
|
||||
user.HumanOTPEmailCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
origin := http_util.DomainContext(ctx).Origin()
|
||||
var authRequestID string
|
||||
if e.AuthRequestInfo != nil {
|
||||
authRequestID = e.AuthRequestInfo.ID
|
||||
}
|
||||
args := otpArgs(ctx, e.Expiry)
|
||||
args.AuthRequestID = authRequestID
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: e.Aggregate().ID,
|
||||
UserResourceOwner: e.Aggregate().ResourceOwner,
|
||||
TriggeredAtOrigin: origin,
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
MessageType: domain.VerifyEmailOTPMessageType,
|
||||
Code: e.Code,
|
||||
CodeExpiry: e.Expiry,
|
||||
IsOTP: true,
|
||||
URLTemplate: login.OTPLinkTemplate(origin, authRequestID, domain.MFATypeOTPEmail),
|
||||
Args: args,
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceSessionOTPEmailChallenged(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.OTPEmailChallengedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-zbsgt", "reduce.wrong.event.type %s", session.OTPEmailChallengedType)
|
||||
}
|
||||
if e.ReturnCode {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
session.OTPEmailChallengedType,
|
||||
session.OTPEmailSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
origin := http_util.DomainContext(ctx).Origin()
|
||||
|
||||
args := otpArgs(ctx, e.Expiry)
|
||||
args.SessionID = e.Aggregate().ID
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: s.UserFactor.UserID,
|
||||
UserResourceOwner: s.UserFactor.ResourceOwner,
|
||||
TriggeredAtOrigin: origin,
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
MessageType: domain.VerifyEmailOTPMessageType,
|
||||
Code: e.Code,
|
||||
CodeExpiry: e.Expiry,
|
||||
IsOTP: true,
|
||||
URLTemplate: u.otpEmailTemplate(origin, e),
|
||||
Args: args,
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) otpEmailTemplate(origin string, e *session.OTPEmailChallengedEvent) string {
|
||||
if e.URLTmpl != "" {
|
||||
return e.URLTmpl
|
||||
}
|
||||
return origin + u.otpEmailTmpl
|
||||
}
|
||||
|
||||
func otpArgs(ctx context.Context, expiry time.Duration) *domain.NotificationArguments {
|
||||
domainCtx := http_util.DomainContext(ctx)
|
||||
return &domain.NotificationArguments{
|
||||
Origin: domainCtx.Origin(),
|
||||
Domain: domainCtx.RequestedDomain(),
|
||||
Expiry: expiry,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceDomainClaimed(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.DomainClaimedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Drh5w", "reduce.wrong.event.type %s", user.UserDomainClaimedType)
|
||||
}
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, nil,
|
||||
user.UserDomainClaimedType, user.UserDomainClaimedSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
origin := http_util.DomainContext(ctx).Origin()
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: e.Aggregate().ID,
|
||||
UserResourceOwner: e.Aggregate().ResourceOwner,
|
||||
TriggeredAtOrigin: origin,
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
MessageType: domain.DomainClaimedMessageType,
|
||||
URLTemplate: login.LoginLink(origin, e.Aggregate().ResourceOwner),
|
||||
UnverifiedNotificationChannel: true,
|
||||
Args: &domain.NotificationArguments{
|
||||
TempUsername: e.UserName,
|
||||
},
|
||||
RequiresPreviousDomain: true,
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) reducePasswordlessCodeRequested(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPasswordlessInitCodeRequestedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-EDtjd", "reduce.wrong.event.type %s", user.HumanPasswordlessInitCodeAddedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, map[string]interface{}{"id": e.ID}, user.HumanPasswordlessInitCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
origin := http_util.DomainContext(ctx).Origin()
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: e.Aggregate().ID,
|
||||
UserResourceOwner: e.Aggregate().ResourceOwner,
|
||||
TriggeredAtOrigin: origin,
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
MessageType: domain.PasswordlessRegistrationMessageType,
|
||||
URLTemplate: u.passwordlessCodeTemplate(origin, e),
|
||||
Args: &domain.NotificationArguments{
|
||||
CodeID: e.ID,
|
||||
},
|
||||
CodeExpiry: e.Expiry,
|
||||
Code: e.Code,
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) passwordlessCodeTemplate(origin string, e *user.HumanPasswordlessInitCodeRequestedEvent) string {
|
||||
if e.URLTemplate != "" {
|
||||
return e.URLTemplate
|
||||
}
|
||||
return domain.PasswordlessInitCodeLinkTemplate(origin+login.HandlerPrefix+login.EndpointPasswordlessRegistration, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.ID)
|
||||
}
|
||||
|
||||
func (u *userNotifier) reducePasswordChanged(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPasswordChangedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Yko2z8", "reduce.wrong.event.type %s", user.HumanPasswordChangedType)
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, nil, user.HumanPasswordChangeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
|
||||
notificationPolicy, err := u.queries.NotificationPolicyByOrg(ctx, true, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil && !zerrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !notificationPolicy.PasswordChange {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
origin := http_util.DomainContext(ctx).Origin()
|
||||
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: e.Aggregate().ID,
|
||||
UserResourceOwner: e.Aggregate().ResourceOwner,
|
||||
TriggeredAtOrigin: origin,
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
MessageType: domain.PasswordChangeMessageType,
|
||||
URLTemplate: console.LoginHintLink(origin, "{{.PreferredLoginName}}"),
|
||||
UnverifiedNotificationChannel: true,
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPhoneCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-He83g", "reduce.wrong.event.type %s", user.HumanPhoneCodeAddedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1PhoneCodeAddedType, user.UserV1PhoneCodeSentType,
|
||||
user.HumanPhoneCodeAddedType, user.HumanPhoneCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: e.Aggregate().ID,
|
||||
UserResourceOwner: e.Aggregate().ResourceOwner,
|
||||
TriggeredAtOrigin: http_util.DomainContext(ctx).Origin(),
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeSms,
|
||||
MessageType: domain.VerifyPhoneMessageType,
|
||||
CodeExpiry: e.Expiry,
|
||||
Code: e.Code,
|
||||
UnverifiedNotificationChannel: true,
|
||||
Args: &domain.NotificationArguments{
|
||||
Domain: http_util.DomainContext(ctx).RequestedDomain(),
|
||||
},
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceInviteCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanInviteCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Eeg3s", "reduce.wrong.event.type %s", user.HumanInviteCodeAddedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.HumanInviteCodeAddedType, user.HumanInviteCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
origin := http_util.DomainContext(ctx).Origin()
|
||||
|
||||
applicationName := e.ApplicationName
|
||||
if applicationName == "" {
|
||||
applicationName = "ZITADEL"
|
||||
}
|
||||
return u.queue.Insert(ctx,
|
||||
¬ification.Request{
|
||||
Aggregate: e.Aggregate(),
|
||||
UserID: e.Aggregate().ID,
|
||||
UserResourceOwner: e.Aggregate().ResourceOwner,
|
||||
TriggeredAtOrigin: origin,
|
||||
EventType: e.EventType,
|
||||
NotificationType: domain.NotificationTypeEmail,
|
||||
MessageType: domain.InviteUserMessageType,
|
||||
CodeExpiry: e.Expiry,
|
||||
Code: e.Code,
|
||||
UnverifiedNotificationChannel: true,
|
||||
URLTemplate: u.inviteCodeTemplate(origin, e),
|
||||
Args: &domain.NotificationArguments{
|
||||
AuthRequestID: e.AuthRequestID,
|
||||
ApplicationName: applicationName,
|
||||
},
|
||||
},
|
||||
queue.WithQueueName(notification.QueueName),
|
||||
queue.WithMaxAttempts(u.maxAttempts),
|
||||
)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) inviteCodeTemplate(origin string, e *user.HumanInviteCodeAddedEvent) string {
|
||||
if e.URLTemplate != "" {
|
||||
return e.URLTemplate
|
||||
}
|
||||
return login.InviteUserLinkTemplate(origin, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.AuthRequestID)
|
||||
}
|
||||
|
||||
func (u *userNotifier) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event eventstore.Event, expiry time.Duration, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) {
|
||||
if expiry > 0 && event.CreatedAt().Add(expiry).Before(time.Now().UTC()) {
|
||||
return true, nil
|
||||
}
|
||||
return u.queries.IsAlreadyHandled(ctx, event, data, eventTypes...)
|
||||
}
|
835
apps/api/internal/notification/handlers/user_notifier_legacy.go
Normal file
835
apps/api/internal/notification/handlers/user_notifier_legacy.go
Normal file
@@ -0,0 +1,835 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
"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/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels"
|
||||
"github.com/zitadel/zitadel/internal/notification/senders"
|
||||
"github.com/zitadel/zitadel/internal/notification/types"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type userNotifierLegacy struct {
|
||||
commands Commands
|
||||
queries *NotificationQueries
|
||||
channels types.ChannelChains
|
||||
otpEmailTmpl string
|
||||
}
|
||||
|
||||
func NewUserNotifierLegacy(
|
||||
ctx context.Context,
|
||||
config handler.Config,
|
||||
commands Commands,
|
||||
queries *NotificationQueries,
|
||||
channels types.ChannelChains,
|
||||
otpEmailTmpl string,
|
||||
) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, &userNotifierLegacy{
|
||||
commands: commands,
|
||||
queries: queries,
|
||||
otpEmailTmpl: otpEmailTmpl,
|
||||
channels: channels,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) Name() string {
|
||||
return UserNotificationsProjectionTable
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: user.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: user.UserV1InitialCodeAddedType,
|
||||
Reduce: u.reduceInitCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanInitialCodeAddedType,
|
||||
Reduce: u.reduceInitCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.UserV1EmailCodeAddedType,
|
||||
Reduce: u.reduceEmailCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanEmailCodeAddedType,
|
||||
Reduce: u.reduceEmailCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.UserV1PasswordCodeAddedType,
|
||||
Reduce: u.reducePasswordCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPasswordCodeAddedType,
|
||||
Reduce: u.reducePasswordCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.UserDomainClaimedType,
|
||||
Reduce: u.reduceDomainClaimed,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPasswordlessInitCodeRequestedType,
|
||||
Reduce: u.reducePasswordlessCodeRequested,
|
||||
},
|
||||
{
|
||||
Event: user.UserV1PhoneCodeAddedType,
|
||||
Reduce: u.reducePhoneCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPhoneCodeAddedType,
|
||||
Reduce: u.reducePhoneCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPasswordChangedType,
|
||||
Reduce: u.reducePasswordChanged,
|
||||
},
|
||||
{
|
||||
Event: user.HumanOTPSMSCodeAddedType,
|
||||
Reduce: u.reduceOTPSMSCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanOTPEmailCodeAddedType,
|
||||
Reduce: u.reduceOTPEmailCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanInviteCodeAddedType,
|
||||
Reduce: u.reduceInviteCodeAdded,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Aggregate: session.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: session.OTPSMSChallengedType,
|
||||
Reduce: u.reduceSessionOTPSMSChallenged,
|
||||
},
|
||||
{
|
||||
Event: session.OTPEmailChallengedType,
|
||||
Reduce: u.reduceSessionOTPEmailChallenged,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reduceInitCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanInitialCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-EFe2f", "reduce.wrong.event.type %s", user.HumanInitialCodeAddedType)
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1InitialCodeAddedType, user.UserV1InitialCodeSentType,
|
||||
user.HumanInitialCodeAddedType, user.HumanInitialCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.InitCodeMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e.Type()).
|
||||
SendUserInitCode(ctx, notifyUser, code, e.AuthRequestID)
|
||||
if err != nil {
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
// if the notification was canceled, we don't want to return the error, so there is no retry
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return u.commands.HumanInitCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reduceEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanEmailCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType)
|
||||
}
|
||||
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType,
|
||||
user.HumanEmailCodeAddedType, user.HumanEmailCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyEmailMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()).
|
||||
SendEmailVerificationCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID)
|
||||
if err != nil {
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
// if the notification was canceled, we don't want to return the error, so there is no retry
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return u.commands.HumanEmailVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reducePasswordCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPasswordCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Eeg3s", "reduce.wrong.event.type %s", user.HumanPasswordCodeAddedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1PasswordCodeAddedType, user.UserV1PasswordCodeSentType,
|
||||
user.HumanPasswordCodeAddedType, user.HumanPasswordCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
var code string
|
||||
if e.Code != nil {
|
||||
code, err = crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordResetMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
generatorInfo := new(senders.CodeGeneratorInfo)
|
||||
notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type())
|
||||
if e.NotificationType == domain.NotificationTypeSms {
|
||||
notify = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e.Type(), e.Aggregate().InstanceID, e.ID, generatorInfo)
|
||||
}
|
||||
err = notify.SendPasswordCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID)
|
||||
if err != nil {
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
// if the notification was canceled, we don't want to return the error, so there is no retry
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return u.commands.PasswordCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID, generatorInfo)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reduceOTPSMSCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanOTPSMSCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ASF3g", "reduce.wrong.event.type %s", user.HumanOTPSMSCodeAddedType)
|
||||
}
|
||||
return u.reduceOTPSMS(
|
||||
e,
|
||||
e.Code,
|
||||
e.Expiry,
|
||||
e.Aggregate().ID,
|
||||
e.Aggregate().ResourceOwner,
|
||||
u.commands.HumanOTPSMSCodeSent,
|
||||
user.HumanOTPSMSCodeAddedType,
|
||||
user.HumanOTPSMSCodeSentType,
|
||||
)
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reduceSessionOTPSMSChallenged(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.OTPSMSChallengedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Sk32L", "reduce.wrong.event.type %s", session.OTPSMSChallengedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
ctx := HandlerContext(context.Background(), event.Aggregate())
|
||||
s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u.reduceOTPSMS(
|
||||
e,
|
||||
e.Code,
|
||||
e.Expiry,
|
||||
s.UserFactor.UserID,
|
||||
s.UserFactor.ResourceOwner,
|
||||
u.commands.OTPSMSSent,
|
||||
session.OTPSMSChallengedType,
|
||||
session.OTPSMSSentType,
|
||||
)
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reduceOTPSMS(
|
||||
event eventstore.Event,
|
||||
code *crypto.CryptoValue,
|
||||
expiry time.Duration,
|
||||
userID,
|
||||
resourceOwner string,
|
||||
sentCommand func(ctx context.Context, userID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) (err error),
|
||||
eventTypes ...eventstore.EventType,
|
||||
) (*handler.Statement, error) {
|
||||
ctx := HandlerContext(context.Background(), event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, expiry, nil, eventTypes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return handler.NewNoOpStatement(event), nil
|
||||
}
|
||||
var plainCode string
|
||||
if code != nil {
|
||||
plainCode, err = crypto.DecryptString(code, u.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, resourceOwner, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifySMSOTPMessageType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, err = u.queries.Origin(ctx, event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
generatorInfo := new(senders.CodeGeneratorInfo)
|
||||
notify := types.SendSMS(ctx, u.channels, translator, notifyUser, colors, event.Type(), event.Aggregate().InstanceID, event.Aggregate().ID, generatorInfo)
|
||||
err = notify.SendOTPSMSCode(ctx, plainCode, expiry)
|
||||
if err != nil {
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
// if the notification was canceled, we don't want to return the error, so there is no retry
|
||||
return handler.NewNoOpStatement(event), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
err = sentCommand(ctx, event.Aggregate().ID, event.Aggregate().ResourceOwner, generatorInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handler.NewNoOpStatement(event), nil
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reduceOTPEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanOTPEmailCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-JL3hw", "reduce.wrong.event.type %s", user.HumanOTPEmailCodeAddedType)
|
||||
}
|
||||
var authRequestID string
|
||||
if e.AuthRequestInfo != nil {
|
||||
authRequestID = e.AuthRequestInfo.ID
|
||||
}
|
||||
url := func(code, origin string, _ *query.NotifyUser) (string, error) {
|
||||
return login.OTPLink(origin, authRequestID, code, domain.MFATypeOTPEmail), nil
|
||||
}
|
||||
return u.reduceOTPEmail(
|
||||
e,
|
||||
e.Code,
|
||||
e.Expiry,
|
||||
e.Aggregate().ID,
|
||||
e.Aggregate().ResourceOwner,
|
||||
url,
|
||||
u.commands.HumanOTPEmailCodeSent,
|
||||
user.HumanOTPEmailCodeAddedType,
|
||||
user.HumanOTPEmailCodeSentType,
|
||||
)
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reduceSessionOTPEmailChallenged(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.OTPEmailChallengedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-zbsgt", "reduce.wrong.event.type %s", session.OTPEmailChallengedType)
|
||||
}
|
||||
if e.ReturnCode {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
ctx := HandlerContext(context.Background(), event.Aggregate())
|
||||
s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := func(code, origin string, user *query.NotifyUser) (string, error) {
|
||||
var buf strings.Builder
|
||||
urlTmpl := origin + u.otpEmailTmpl
|
||||
if e.URLTmpl != "" {
|
||||
urlTmpl = e.URLTmpl
|
||||
}
|
||||
if err := domain.RenderOTPEmailURLTemplate(&buf, urlTmpl, code, user.ID, user.PreferredLoginName, user.DisplayName, e.Aggregate().ID, user.PreferredLanguage); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
return u.reduceOTPEmail(
|
||||
e,
|
||||
e.Code,
|
||||
e.Expiry,
|
||||
s.UserFactor.UserID,
|
||||
s.UserFactor.ResourceOwner,
|
||||
url,
|
||||
u.commands.OTPEmailSent,
|
||||
user.HumanOTPEmailCodeAddedType,
|
||||
user.HumanOTPEmailCodeSentType,
|
||||
)
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reduceOTPEmail(
|
||||
event eventstore.Event,
|
||||
code *crypto.CryptoValue,
|
||||
expiry time.Duration,
|
||||
userID,
|
||||
resourceOwner string,
|
||||
urlTmpl func(code, origin string, user *query.NotifyUser) (string, error),
|
||||
sentCommand func(ctx context.Context, userID string, resourceOwner string) (err error),
|
||||
eventTypes ...eventstore.EventType,
|
||||
) (*handler.Statement, error) {
|
||||
ctx := HandlerContext(context.Background(), event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, expiry, nil, eventTypes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return handler.NewNoOpStatement(event), nil
|
||||
}
|
||||
plainCode, err := crypto.DecryptString(code, u.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, resourceOwner, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template, err := u.queries.MailTemplateByOrg(ctx, resourceOwner, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, resourceOwner, domain.VerifyEmailOTPMessageType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, err = u.queries.Origin(ctx, event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url, err := urlTmpl(plainCode, http_util.DomainContext(ctx).Origin(), notifyUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type())
|
||||
err = notify.SendOTPEmailCode(ctx, url, plainCode, expiry)
|
||||
if err != nil {
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
// if the notification was canceled, we don't want to return the error, so there is no retry
|
||||
return handler.NewNoOpStatement(event), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
err = sentCommand(ctx, event.Aggregate().ID, event.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handler.NewNoOpStatement(event), nil
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reduceDomainClaimed(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.DomainClaimedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Drh5w", "reduce.wrong.event.type %s", user.UserDomainClaimedType)
|
||||
}
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, nil,
|
||||
user.UserDomainClaimedType, user.UserDomainClaimedSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.DomainClaimedMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()).
|
||||
SendDomainClaimed(ctx, notifyUser, e.UserName)
|
||||
if err != nil {
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
// if the notification was canceled, we don't want to return the error, so there is no retry
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return u.commands.UserDomainClaimedSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reducePasswordlessCodeRequested(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPasswordlessInitCodeRequestedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-EDtjd", "reduce.wrong.event.type %s", user.HumanPasswordlessInitCodeAddedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, map[string]interface{}{"id": e.ID}, user.HumanPasswordlessInitCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordlessRegistrationMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()).
|
||||
SendPasswordlessRegistrationLink(ctx, notifyUser, code, e.ID, e.URLTemplate)
|
||||
if err != nil {
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
// if the notification was canceled, we don't want to return the error, so there is no retry
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return u.commands.HumanPasswordlessInitCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.ID)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reducePasswordChanged(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPasswordChangedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Yko2z8", "reduce.wrong.event.type %s", user.HumanPasswordChangedType)
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.queries.IsAlreadyHandled(ctx, event, nil, user.HumanPasswordChangeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
|
||||
notificationPolicy, err := u.queries.NotificationPolicyByOrg(ctx, true, e.Aggregate().ResourceOwner, false)
|
||||
if zerrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !notificationPolicy.PasswordChange {
|
||||
return nil
|
||||
}
|
||||
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordChangeMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type()).
|
||||
SendPasswordChange(ctx, notifyUser)
|
||||
if err != nil {
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
// if the notification was canceled, we don't want to return the error, so there is no retry
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return u.commands.PasswordChangeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reducePhoneCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPhoneCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-He83g", "reduce.wrong.event.type %s", user.HumanPhoneCodeAddedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1PhoneCodeAddedType, user.UserV1PhoneCodeSentType,
|
||||
user.HumanPhoneCodeAddedType, user.HumanPhoneCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
var code string
|
||||
if e.Code != nil {
|
||||
code, err = crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyPhoneMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
generatorInfo := new(senders.CodeGeneratorInfo)
|
||||
if err = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e.Type(), e.Aggregate().InstanceID, e.ID, generatorInfo).
|
||||
SendPhoneVerificationCode(ctx, code); err != nil {
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
// if the notification was canceled, we don't want to return the error, so there is no retry
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return u.commands.HumanPhoneVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID, generatorInfo)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) reduceInviteCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanInviteCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Eeg3s", "reduce.wrong.event.type %s", user.HumanInviteCodeAddedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, projectionName string) error {
|
||||
ctx = HandlerContext(ctx, event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.HumanInviteCodeAddedType, user.HumanInviteCodeSentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.InviteUserMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, err = u.queries.Origin(ctx, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, event.Type())
|
||||
err = notify.SendInviteCode(ctx, notifyUser, code, e.ApplicationName, e.URLTemplate, e.AuthRequestID)
|
||||
if err != nil {
|
||||
if errors.Is(err, &channels.CancelError{}) {
|
||||
// if the notification was canceled, we don't want to return the error, so there is no retry
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return u.commands.InviteCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (u *userNotifierLegacy) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event eventstore.Event, expiry time.Duration, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) {
|
||||
if expiry > 0 && event.CreatedAt().Add(expiry).Before(time.Now().UTC()) {
|
||||
return true, nil
|
||||
}
|
||||
return u.queries.IsAlreadyHandled(ctx, event, data, eventTypes...)
|
||||
}
|
2010
apps/api/internal/notification/handlers/user_notifier_legacy_test.go
Normal file
2010
apps/api/internal/notification/handlers/user_notifier_legacy_test.go
Normal file
File diff suppressed because it is too large
Load Diff
2038
apps/api/internal/notification/handlers/user_notifier_test.go
Normal file
2038
apps/api/internal/notification/handlers/user_notifier_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user