mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-13 11:34:26 +00:00
fix(notifications): bring back legacy notification handling (#9015)
# Which Problems Are Solved There are some problems related to the use of CockroachDB with the new notification handling (#8931). See #9002 for details. # How the Problems Are Solved - Brought back the previous notification handler as legacy mode. - Added a configuration to choose between legacy mode and new parallel workers. - Enabled legacy mode by default to prevent issues. # Additional Changes None # Additional Context - closes https://github.com/zitadel/zitadel/issues/9002 - relates to #8931
This commit is contained in:
parent
71d381b5e7
commit
7a3ae8f499
@ -449,6 +449,11 @@ Projections:
|
||||
RequeueEvery: 3300s # ZITADEL_PROJECTIONS_CUSTOMIZATIONS_TELEMETRY_REQUEUEEVERY
|
||||
|
||||
Notifications:
|
||||
# Notifications can be processed by either a sequential mode (legacy) or a new parallel mode.
|
||||
# The parallel mode is currently only recommended for Postgres databases.
|
||||
# For CockroachDB, the sequential mode is recommended, see: https://github.com/zitadel/zitadel/issues/9002
|
||||
# If legacy mode is enabled, the worker config below is ignored.
|
||||
LegacyEnabled: true # ZITADEL_NOTIFICATIONS_LEGACYENABLED
|
||||
# The amount of workers processing the notification request events.
|
||||
# If set to 0, no notification request events will be handled. This can be useful when running in
|
||||
# multi binary / pod setup and allowing only certain executables to process the events.
|
||||
|
@ -3,6 +3,7 @@ package login
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@ -38,6 +39,15 @@ type initPasswordData struct {
|
||||
HasSymbol string
|
||||
}
|
||||
|
||||
func InitPasswordLink(origin, userID, code, orgID, authRequestID string) string {
|
||||
v := url.Values{}
|
||||
v.Set(queryInitPWUserID, userID)
|
||||
v.Set(queryInitPWCode, code)
|
||||
v.Set(queryOrgID, orgID)
|
||||
v.Set(QueryAuthRequestID, authRequestID)
|
||||
return externalLink(origin) + EndpointInitPassword + "?" + v.Encode()
|
||||
}
|
||||
|
||||
func InitPasswordLinkTemplate(origin, userID, orgID, authRequestID string) string {
|
||||
return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%s&%s=%s",
|
||||
externalLink(origin), EndpointInitPassword,
|
||||
|
@ -3,6 +3,7 @@ package login
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
@ -44,6 +45,17 @@ type initUserData struct {
|
||||
HasSymbol string
|
||||
}
|
||||
|
||||
func InitUserLink(origin, userID, loginName, code, orgID string, passwordSet bool, authRequestID string) string {
|
||||
v := url.Values{}
|
||||
v.Set(queryInitUserUserID, userID)
|
||||
v.Set(queryInitUserLoginName, loginName)
|
||||
v.Set(queryInitUserCode, code)
|
||||
v.Set(queryOrgID, orgID)
|
||||
v.Set(queryInitUserPassword, strconv.FormatBool(passwordSet))
|
||||
v.Set(QueryAuthRequestID, authRequestID)
|
||||
return externalLink(origin) + EndpointInitUser + "?" + v.Encode()
|
||||
}
|
||||
|
||||
func InitUserLinkTemplate(origin, userID, orgID, authRequestID string) string {
|
||||
return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%s&%s=%s&%s=%s&%s=%s",
|
||||
externalLink(origin), EndpointInitUser,
|
||||
|
@ -3,6 +3,7 @@ package login
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@ -40,6 +41,16 @@ type inviteUserData struct {
|
||||
HasSymbol string
|
||||
}
|
||||
|
||||
func InviteUserLink(origin, userID, loginName, code, orgID string, authRequestID string) string {
|
||||
v := url.Values{}
|
||||
v.Set(queryInviteUserUserID, userID)
|
||||
v.Set(queryInviteUserLoginName, loginName)
|
||||
v.Set(queryInviteUserCode, code)
|
||||
v.Set(queryOrgID, orgID)
|
||||
v.Set(QueryAuthRequestID, authRequestID)
|
||||
return externalLink(origin) + EndpointInviteUser + "?" + v.Encode()
|
||||
}
|
||||
|
||||
func InviteUserLinkTemplate(origin, userID, orgID string, authRequestID string) string {
|
||||
return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%s&%s=%s&%s=%s",
|
||||
externalLink(origin), EndpointInviteUser,
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
@ -43,6 +44,15 @@ type mailVerificationData struct {
|
||||
HasSymbol string
|
||||
}
|
||||
|
||||
func MailVerificationLink(origin, userID, code, orgID, authRequestID string) string {
|
||||
v := url.Values{}
|
||||
v.Set(queryUserID, userID)
|
||||
v.Set(queryCode, code)
|
||||
v.Set(queryOrgID, orgID)
|
||||
v.Set(QueryAuthRequestID, authRequestID)
|
||||
return externalLink(origin) + EndpointMailVerification + "?" + v.Encode()
|
||||
}
|
||||
|
||||
func MailVerificationLinkTemplate(origin, userID, orgID, authRequestID string) string {
|
||||
return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%s&%s=%s",
|
||||
externalLink(origin), EndpointMailVerification,
|
||||
|
@ -27,6 +27,10 @@ type mfaOTPFormData struct {
|
||||
Provider domain.MFAType `schema:"provider"`
|
||||
}
|
||||
|
||||
func OTPLink(origin, authRequestID, code string, provider domain.MFAType) string {
|
||||
return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%d", externalLink(origin), EndpointMFAOTPVerify, QueryAuthRequestID, authRequestID, queryCode, code, querySelectedProvider, provider)
|
||||
}
|
||||
|
||||
func OTPLinkTemplate(origin, authRequestID string, provider domain.MFAType) string {
|
||||
return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%d", externalLink(origin), EndpointMFAOTPVerify, QueryAuthRequestID, authRequestID, queryCode, "{{.Code}}", querySelectedProvider, provider)
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ type NotificationWorker struct {
|
||||
}
|
||||
|
||||
type WorkerConfig struct {
|
||||
LegacyEnabled bool
|
||||
Workers uint8
|
||||
BulkLimit uint16
|
||||
RequeueEvery time.Duration
|
||||
@ -97,6 +98,9 @@ func NewNotificationWorker(
|
||||
}
|
||||
|
||||
func (w *NotificationWorker) Start(ctx context.Context) {
|
||||
if w.config.LegacyEnabled {
|
||||
return
|
||||
}
|
||||
for i := 0; i < int(w.config.Workers); i++ {
|
||||
go w.schedule(ctx, i, false)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"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/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
@ -95,8 +96,13 @@ func NewUserNotifier(
|
||||
config handler.Config,
|
||||
commands Commands,
|
||||
queries *NotificationQueries,
|
||||
channels types.ChannelChains,
|
||||
otpEmailTmpl string,
|
||||
legacyMode bool,
|
||||
) *handler.Handler {
|
||||
if legacyMode {
|
||||
return NewUserNotifierLegacy(ctx, config, commands, queries, channels, otpEmailTmpl)
|
||||
}
|
||||
return handler.NewHandler(ctx, &config, &userNotifier{
|
||||
commands: commands,
|
||||
queries: queries,
|
||||
|
793
internal/notification/handlers/user_notifier_legacy.go
Normal file
793
internal/notification/handlers/user_notifier_legacy.go
Normal file
@ -0,0 +1,793 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/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(ex handler.Executer, projectionName string) error {
|
||||
ctx := HandlerContext(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).
|
||||
SendUserInitCode(ctx, notifyUser, code, e.AuthRequestID)
|
||||
if err != 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(ex handler.Executer, projectionName string) error {
|
||||
ctx := HandlerContext(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, e).
|
||||
SendEmailVerificationCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID)
|
||||
if err != 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(ex handler.Executer, projectionName string) error {
|
||||
ctx := HandlerContext(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, e)
|
||||
if e.NotificationType == domain.NotificationTypeSms {
|
||||
notify = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e, generatorInfo)
|
||||
}
|
||||
err = notify.SendPasswordCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID)
|
||||
if err != 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(event.Aggregate())
|
||||
s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "")
|
||||
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(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, generatorInfo)
|
||||
err = notify.SendOTPSMSCode(ctx, plainCode, expiry)
|
||||
if err != 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(event.Aggregate())
|
||||
s, err := u.queries.SessionByID(ctx, true, e.Aggregate().ID, "")
|
||||
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(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)
|
||||
err = notify.SendOTPEmailCode(ctx, url, plainCode, expiry)
|
||||
if err != 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(ex handler.Executer, projectionName string) error {
|
||||
ctx := HandlerContext(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, e).
|
||||
SendDomainClaimed(ctx, notifyUser, e.UserName)
|
||||
if err != 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(ex handler.Executer, projectionName string) error {
|
||||
ctx := HandlerContext(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, e).
|
||||
SendPasswordlessRegistrationLink(ctx, notifyUser, code, e.ID, e.URLTemplate)
|
||||
if err != 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(ex handler.Executer, projectionName string) error {
|
||||
ctx := HandlerContext(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, e).
|
||||
SendPasswordChange(ctx, notifyUser)
|
||||
if err != 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(ex handler.Executer, projectionName string) error {
|
||||
ctx := HandlerContext(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, generatorInfo).
|
||||
SendPhoneVerificationCode(ctx, code); err != 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(ex handler.Executer, projectionName string) error {
|
||||
ctx := HandlerContext(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, e)
|
||||
err = notify.SendInviteCode(ctx, notifyUser, code, e.ApplicationName, e.URLTemplate, e.AuthRequestID)
|
||||
if err != 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...)
|
||||
}
|
1601
internal/notification/handlers/user_notifier_legacy_test.go
Normal file
1601
internal/notification/handlers/user_notifier_legacy_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@ func Register(
|
||||
) {
|
||||
q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption)
|
||||
c := newChannels(q)
|
||||
projections = append(projections, handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, otpEmailTmpl))
|
||||
projections = append(projections, handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl, notificationWorkerConfig.LegacyEnabled))
|
||||
projections = append(projections, handlers.NewQuotaNotifier(ctx, projection.ApplyCustomConfig(quotaHandlerCustomConfig), commands, q, c))
|
||||
projections = append(projections, handlers.NewBackChannelLogoutNotifier(
|
||||
ctx,
|
||||
|
20
internal/notification/types/domain_claimed.go
Normal file
20
internal/notification/types/domain_claimed.go
Normal file
@ -0,0 +1,20 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendDomainClaimed(ctx context.Context, user *query.NotifyUser, username string) error {
|
||||
url := login.LoginLink(http_utils.DomainContext(ctx).Origin(), user.ResourceOwner)
|
||||
index := strings.LastIndex(user.LastEmail, "@")
|
||||
args := make(map[string]interface{})
|
||||
args["TempUsername"] = username
|
||||
args["Domain"] = user.LastEmail[index+1:]
|
||||
return notify(url, args, domain.DomainClaimedMessageType, true)
|
||||
}
|
28
internal/notification/types/email_verification_code.go
Normal file
28
internal/notification/types/email_verification_code.go
Normal file
@ -0,0 +1,28 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendEmailVerificationCode(ctx context.Context, user *query.NotifyUser, code string, urlTmpl, authRequestID string) error {
|
||||
var url string
|
||||
if urlTmpl == "" {
|
||||
url = login.MailVerificationLink(http_utils.DomainContext(ctx).Origin(), user.ID, code, user.ResourceOwner, authRequestID)
|
||||
} else {
|
||||
var buf strings.Builder
|
||||
if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil {
|
||||
return err
|
||||
}
|
||||
url = buf.String()
|
||||
}
|
||||
|
||||
args := make(map[string]interface{})
|
||||
args["Code"] = code
|
||||
return notify(url, args, domain.VerifyEmailMessageType, true)
|
||||
}
|
92
internal/notification/types/email_verification_code_test.go
Normal file
92
internal/notification/types/email_verification_code_test.go
Normal file
@ -0,0 +1,92 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestNotify_SendEmailVerificationCode(t *testing.T) {
|
||||
type args struct {
|
||||
user *query.NotifyUser
|
||||
origin *http_utils.DomainCtx
|
||||
code string
|
||||
urlTmpl string
|
||||
authRequestID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *notifyResult
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "default URL",
|
||||
args: args{
|
||||
user: &query.NotifyUser{
|
||||
ID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"},
|
||||
code: "123",
|
||||
urlTmpl: "",
|
||||
authRequestID: "authRequestID",
|
||||
},
|
||||
want: ¬ifyResult{
|
||||
url: "https://example.com/ui/login/mail/verification?authRequestID=authRequestID&code=123&orgID=org1&userID=user1",
|
||||
args: map[string]interface{}{"Code": "123"},
|
||||
messageType: domain.VerifyEmailMessageType,
|
||||
allowUnverifiedNotificationChannel: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "template error",
|
||||
args: args{
|
||||
user: &query.NotifyUser{
|
||||
ID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"},
|
||||
code: "123",
|
||||
urlTmpl: "{{",
|
||||
authRequestID: "authRequestID",
|
||||
},
|
||||
want: ¬ifyResult{},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "template success",
|
||||
args: args{
|
||||
user: &query.NotifyUser{
|
||||
ID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"},
|
||||
code: "123",
|
||||
urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||
authRequestID: "authRequestID",
|
||||
},
|
||||
want: ¬ifyResult{
|
||||
url: "https://example.com/email/verify?userID=user1&code=123&orgID=org1",
|
||||
args: map[string]interface{}{"Code": "123"},
|
||||
messageType: domain.VerifyEmailMessageType,
|
||||
allowUnverifiedNotificationChannel: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, notify := mockNotify()
|
||||
err := notify.SendEmailVerificationCode(http_utils.WithDomainContext(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.urlTmpl, tt.args.authRequestID)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
17
internal/notification/types/init_code.go
Normal file
17
internal/notification/types/init_code.go
Normal file
@ -0,0 +1,17 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendUserInitCode(ctx context.Context, user *query.NotifyUser, code, authRequestID string) error {
|
||||
url := login.InitUserLink(http_utils.DomainContext(ctx).Origin(), user.ID, user.PreferredLoginName, code, user.ResourceOwner, user.PasswordSet, authRequestID)
|
||||
args := make(map[string]interface{})
|
||||
args["Code"] = code
|
||||
return notify(url, args, domain.InitCodeMessageType, true)
|
||||
}
|
31
internal/notification/types/invite_code.go
Normal file
31
internal/notification/types/invite_code.go
Normal file
@ -0,0 +1,31 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendInviteCode(ctx context.Context, user *query.NotifyUser, code, applicationName, urlTmpl, authRequestID string) error {
|
||||
var url string
|
||||
if applicationName == "" {
|
||||
applicationName = "ZITADEL"
|
||||
}
|
||||
if urlTmpl == "" {
|
||||
url = login.InviteUserLink(http_utils.DomainContext(ctx).Origin(), user.ID, user.PreferredLoginName, code, user.ResourceOwner, authRequestID)
|
||||
} else {
|
||||
var buf strings.Builder
|
||||
if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil {
|
||||
return err
|
||||
}
|
||||
url = buf.String()
|
||||
}
|
||||
args := make(map[string]interface{})
|
||||
args["Code"] = code
|
||||
args["ApplicationName"] = applicationName
|
||||
return notify(url, args, domain.InviteUserMessageType, true)
|
||||
}
|
29
internal/notification/types/otp.go
Normal file
29
internal/notification/types/otp.go
Normal file
@ -0,0 +1,29 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
func (notify Notify) SendOTPSMSCode(ctx context.Context, code string, expiry time.Duration) error {
|
||||
args := otpArgs(ctx, code, expiry)
|
||||
return notify("", args, domain.VerifySMSOTPMessageType, false)
|
||||
}
|
||||
|
||||
func (notify Notify) SendOTPEmailCode(ctx context.Context, url, code string, expiry time.Duration) error {
|
||||
args := otpArgs(ctx, code, expiry)
|
||||
return notify(url, args, domain.VerifyEmailOTPMessageType, false)
|
||||
}
|
||||
|
||||
func otpArgs(ctx context.Context, code string, expiry time.Duration) map[string]interface{} {
|
||||
domainCtx := http_utils.DomainContext(ctx)
|
||||
args := make(map[string]interface{})
|
||||
args["OTP"] = code
|
||||
args["Origin"] = domainCtx.Origin()
|
||||
args["Domain"] = domainCtx.RequestedDomain()
|
||||
args["Expiry"] = expiry
|
||||
return args
|
||||
}
|
16
internal/notification/types/password_change.go
Normal file
16
internal/notification/types/password_change.go
Normal file
@ -0,0 +1,16 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/console"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendPasswordChange(ctx context.Context, user *query.NotifyUser) error {
|
||||
url := console.LoginHintLink(http_utils.DomainContext(ctx).Origin(), user.PreferredLoginName)
|
||||
args := make(map[string]interface{})
|
||||
return notify(url, args, domain.PasswordChangeMessageType, true)
|
||||
}
|
27
internal/notification/types/password_code.go
Normal file
27
internal/notification/types/password_code.go
Normal file
@ -0,0 +1,27 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendPasswordCode(ctx context.Context, user *query.NotifyUser, code, urlTmpl, authRequestID string) error {
|
||||
var url string
|
||||
if urlTmpl == "" {
|
||||
url = login.InitPasswordLink(http_utils.DomainContext(ctx).Origin(), user.ID, code, user.ResourceOwner, authRequestID)
|
||||
} else {
|
||||
var buf strings.Builder
|
||||
if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil {
|
||||
return err
|
||||
}
|
||||
url = buf.String()
|
||||
}
|
||||
args := make(map[string]interface{})
|
||||
args["Code"] = code
|
||||
return notify(url, args, domain.PasswordResetMessageType, true)
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendPasswordlessRegistrationLink(ctx context.Context, user *query.NotifyUser, code, codeID, urlTmpl string) error {
|
||||
var url string
|
||||
if urlTmpl == "" {
|
||||
url = domain.PasswordlessInitCodeLink(http_utils.DomainContext(ctx).Origin()+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code)
|
||||
} else {
|
||||
var buf strings.Builder
|
||||
if err := domain.RenderPasskeyURLTemplate(&buf, urlTmpl, user.ID, user.ResourceOwner, codeID, code); err != nil {
|
||||
return err
|
||||
}
|
||||
url = buf.String()
|
||||
}
|
||||
return notify(url, nil, domain.PasswordlessRegistrationMessageType, true)
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) {
|
||||
type args struct {
|
||||
user *query.NotifyUser
|
||||
origin *http_utils.DomainCtx
|
||||
code string
|
||||
codeID string
|
||||
urlTmpl string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *notifyResult
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "default URL",
|
||||
args: args{
|
||||
user: &query.NotifyUser{
|
||||
ID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"},
|
||||
code: "123",
|
||||
codeID: "456",
|
||||
urlTmpl: "",
|
||||
},
|
||||
want: ¬ifyResult{
|
||||
url: "https://example.com/ui/login/login/passwordless/init?userID=user1&orgID=org1&codeID=456&code=123",
|
||||
messageType: domain.PasswordlessRegistrationMessageType,
|
||||
allowUnverifiedNotificationChannel: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "template error",
|
||||
args: args{
|
||||
user: &query.NotifyUser{
|
||||
ID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"},
|
||||
code: "123",
|
||||
codeID: "456",
|
||||
urlTmpl: "{{",
|
||||
},
|
||||
want: ¬ifyResult{},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "template success",
|
||||
args: args{
|
||||
user: &query.NotifyUser{
|
||||
ID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"},
|
||||
code: "123",
|
||||
codeID: "456",
|
||||
urlTmpl: "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}",
|
||||
},
|
||||
want: ¬ifyResult{
|
||||
url: "https://example.com/passkey/register?userID=user1&orgID=org1&codeID=456&code=123",
|
||||
messageType: domain.PasswordlessRegistrationMessageType,
|
||||
allowUnverifiedNotificationChannel: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, notify := mockNotify()
|
||||
err := notify.SendPasswordlessRegistrationLink(http_utils.WithDomainContext(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.codeID, tt.args.urlTmpl)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
15
internal/notification/types/phone_verification_code.go
Normal file
15
internal/notification/types/phone_verification_code.go
Normal file
@ -0,0 +1,15 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
func (notify Notify) SendPhoneVerificationCode(ctx context.Context, code string) error {
|
||||
args := make(map[string]interface{})
|
||||
args["Code"] = code
|
||||
args["Domain"] = http_util.DomainContext(ctx).RequestedDomain()
|
||||
return notify("", args, domain.VerifyPhoneMessageType, true)
|
||||
}
|
23
internal/notification/types/types_test.go
Normal file
23
internal/notification/types/types_test.go
Normal file
@ -0,0 +1,23 @@
|
||||
package types
|
||||
|
||||
type notifyResult struct {
|
||||
url string
|
||||
args map[string]interface{}
|
||||
messageType string
|
||||
allowUnverifiedNotificationChannel bool
|
||||
}
|
||||
|
||||
// mockNotify returns a notifyResult and Notify function for easy mocking.
|
||||
// The notifyResult will only be populated after Notify is called.
|
||||
func mockNotify() (*notifyResult, Notify) {
|
||||
dst := new(notifyResult)
|
||||
return dst, func(url string, args map[string]interface{}, messageType string, allowUnverifiedNotificationChannel bool) error {
|
||||
*dst = notifyResult{
|
||||
url: url,
|
||||
args: args,
|
||||
messageType: messageType,
|
||||
allowUnverifiedNotificationChannel: allowUnverifiedNotificationChannel,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user