mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 19:17:32 +00:00
fix(notify): notify user in projection (#3889)
* start implement notify user in projection * fix(stmt): add copy to multi stmt * use projections for notify users * feat: notifications from projections * feat: notifications from projections * cleanup * pre-release * fix tests * fix types * fix command * fix queryNotifyUser * fix: build version * fix: HumanPasswordlessInitCodeSent Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/rakyll/statik/fs"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/notification/repository/eventsourcing"
|
||||
_ "github.com/zitadel/zitadel/internal/notification/statik"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Repository eventsourcing.Config
|
||||
}
|
||||
|
||||
func Start(config Config,
|
||||
externalPort uint16,
|
||||
externalSecure bool,
|
||||
command *command.Commands,
|
||||
queries *query.Queries,
|
||||
dbClient *sql.DB,
|
||||
assetsPrefix,
|
||||
fileSystemPath string,
|
||||
userEncryption crypto.EncryptionAlgorithm,
|
||||
smtpEncryption crypto.EncryptionAlgorithm,
|
||||
smsEncryption crypto.EncryptionAlgorithm,
|
||||
) {
|
||||
statikFS, err := fs.NewWithNamespace("notification")
|
||||
logging.OnError(err).Panic("unable to start listener")
|
||||
|
||||
_, err = eventsourcing.Start(config.Repository, statikFS, externalPort, externalSecure, command, queries, dbClient, assetsPrefix, fileSystemPath, userEncryption, smtpEncryption, smsEncryption)
|
||||
logging.OnError(err).Panic("unable to start app")
|
||||
}
|
663
internal/notification/projection.go
Normal file
663
internal/notification/projection.go
Normal file
@@ -0,0 +1,663 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
statik_fs "github.com/rakyll/statik/fs"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
|
||||
_ "github.com/zitadel/zitadel/internal/notification/statik"
|
||||
"github.com/zitadel/zitadel/internal/notification/types"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
const (
|
||||
NotificationsProjectionTable = "projections.notifications"
|
||||
NotifyUserID = "NOTIFICATION" //TODO: system?
|
||||
)
|
||||
|
||||
func Start(ctx context.Context, customConfig projection.CustomConfig, externalPort uint16, externalSecure bool, commands *command.Commands, queries *query.Queries, es *eventstore.Eventstore, assetsPrefix func(context.Context) string, fileSystemPath string, userEncryption, smtpEncryption, smsEncryption crypto.EncryptionAlgorithm) {
|
||||
statikFS, err := statik_fs.NewWithNamespace("notification")
|
||||
logging.OnError(err).Panic("unable to start listener")
|
||||
|
||||
projection.NotificationsProjection = newNotificationsProjection(ctx, projection.ApplyCustomConfig(customConfig), commands, queries, es, userEncryption, smtpEncryption, smsEncryption, externalSecure, externalPort, fileSystemPath, assetsPrefix, statikFS)
|
||||
}
|
||||
|
||||
type notificationsProjection struct {
|
||||
crdb.StatementHandler
|
||||
commands *command.Commands
|
||||
queries *query.Queries
|
||||
es *eventstore.Eventstore
|
||||
userDataCrypto crypto.EncryptionAlgorithm
|
||||
smtpPasswordCrypto crypto.EncryptionAlgorithm
|
||||
smsTokenCrypto crypto.EncryptionAlgorithm
|
||||
assetsPrefix func(context.Context) string
|
||||
fileSystemPath string
|
||||
externalPort uint16
|
||||
externalSecure bool
|
||||
statikDir http.FileSystem
|
||||
}
|
||||
|
||||
func newNotificationsProjection(
|
||||
ctx context.Context,
|
||||
config crdb.StatementHandlerConfig,
|
||||
commands *command.Commands,
|
||||
queries *query.Queries,
|
||||
es *eventstore.Eventstore,
|
||||
userDataCrypto,
|
||||
smtpPasswordCrypto,
|
||||
smsTokenCrypto crypto.EncryptionAlgorithm,
|
||||
externalSecure bool,
|
||||
externalPort uint16,
|
||||
fileSystemPath string,
|
||||
assetsPrefix func(context.Context) string,
|
||||
statikDir http.FileSystem,
|
||||
) *notificationsProjection {
|
||||
p := new(notificationsProjection)
|
||||
config.ProjectionName = NotificationsProjectionTable
|
||||
config.Reducers = p.reducers()
|
||||
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
|
||||
p.commands = commands
|
||||
p.queries = queries
|
||||
p.es = es
|
||||
p.userDataCrypto = userDataCrypto
|
||||
p.smtpPasswordCrypto = smtpPasswordCrypto
|
||||
p.smsTokenCrypto = smsTokenCrypto
|
||||
p.assetsPrefix = assetsPrefix
|
||||
p.externalPort = externalPort
|
||||
p.externalSecure = externalSecure
|
||||
p.fileSystemPath = fileSystemPath
|
||||
p.statikDir = statikDir
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: user.AggregateType,
|
||||
EventRedusers: []handler.EventReducer{
|
||||
{
|
||||
Event: user.UserV1InitialCodeAddedType,
|
||||
Reduce: p.reduceInitCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanInitialCodeAddedType,
|
||||
Reduce: p.reduceInitCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.UserV1EmailCodeAddedType,
|
||||
Reduce: p.reduceEmailCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanEmailCodeAddedType,
|
||||
Reduce: p.reduceEmailCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.UserV1PasswordCodeAddedType,
|
||||
Reduce: p.reducePasswordCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPasswordCodeAddedType,
|
||||
Reduce: p.reducePasswordCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.UserDomainClaimedType,
|
||||
Reduce: p.reduceDomainClaimed,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPasswordlessInitCodeRequestedType,
|
||||
Reduce: p.reducePasswordlessCodeRequested,
|
||||
},
|
||||
{
|
||||
Event: user.UserV1PhoneCodeAddedType,
|
||||
Reduce: p.reducePhoneCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPhoneCodeAddedType,
|
||||
Reduce: p.reducePhoneCodeAdded,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) reduceInitCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanInitialCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-EFe2f", "reduce.wrong.event.type %s", user.HumanInitialCodeAddedType)
|
||||
}
|
||||
ctx := setNotificationContext(event.Aggregate())
|
||||
alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1InitialCodeAddedType, user.UserV1InitialCodeSentType,
|
||||
user.HumanInitialCodeAddedType, user.HumanInitialCodeSentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, p.userDataCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.InitCodeMessageType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
origin, err := p.origin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = types.SendEmail(
|
||||
ctx,
|
||||
string(template.Template),
|
||||
translator,
|
||||
notifyUser,
|
||||
p.getSMTPConfig,
|
||||
p.getFileSystemProvider,
|
||||
p.getLogProvider,
|
||||
colors,
|
||||
p.assetsPrefix(ctx),
|
||||
).SendUserInitCode(notifyUser, origin, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = p.commands.HumanInitCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) reduceEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanEmailCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType)
|
||||
}
|
||||
ctx := setNotificationContext(event.Aggregate())
|
||||
alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType,
|
||||
user.HumanEmailCodeAddedType, user.HumanEmailCodeSentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, p.userDataCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyEmailMessageType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
origin, err := p.origin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = types.SendEmail(
|
||||
ctx,
|
||||
string(template.Template),
|
||||
translator,
|
||||
notifyUser,
|
||||
p.getSMTPConfig,
|
||||
p.getFileSystemProvider,
|
||||
p.getLogProvider,
|
||||
colors,
|
||||
p.assetsPrefix(ctx),
|
||||
).SendEmailVerificationCode(notifyUser, origin, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = p.commands.HumanEmailVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) reducePasswordCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPasswordCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Eeg3s", "reduce.wrong.event.type %s", user.HumanPasswordCodeAddedType)
|
||||
}
|
||||
ctx := setNotificationContext(event.Aggregate())
|
||||
alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1PasswordCodeAddedType, user.UserV1PasswordCodeSentType,
|
||||
user.HumanPasswordCodeAddedType, user.HumanPasswordCodeSentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, p.userDataCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordResetMessageType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
origin, err := p.origin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notify := types.SendEmail(
|
||||
ctx,
|
||||
string(template.Template),
|
||||
translator,
|
||||
notifyUser,
|
||||
p.getSMTPConfig,
|
||||
p.getFileSystemProvider,
|
||||
p.getLogProvider,
|
||||
colors,
|
||||
p.assetsPrefix(ctx),
|
||||
)
|
||||
if e.NotificationType == domain.NotificationTypeSms {
|
||||
notify = types.SendSMSTwilio(
|
||||
ctx,
|
||||
translator,
|
||||
notifyUser,
|
||||
p.getTwilioConfig,
|
||||
p.getFileSystemProvider,
|
||||
p.getLogProvider,
|
||||
colors,
|
||||
p.assetsPrefix(ctx),
|
||||
)
|
||||
}
|
||||
err = notify.SendPasswordCode(notifyUser, origin, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = p.commands.PasswordCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) reduceDomainClaimed(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.DomainClaimedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Drh5w", "reduce.wrong.event.type %s", user.UserDomainClaimedType)
|
||||
}
|
||||
ctx := setNotificationContext(event.Aggregate())
|
||||
alreadyHandled, err := p.checkIfAlreadyHandled(ctx, event, nil,
|
||||
user.UserDomainClaimedType, user.UserDomainClaimedSentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.DomainClaimedMessageType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
origin, err := p.origin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = types.SendEmail(
|
||||
ctx,
|
||||
string(template.Template),
|
||||
translator,
|
||||
notifyUser,
|
||||
p.getSMTPConfig,
|
||||
p.getFileSystemProvider,
|
||||
p.getLogProvider,
|
||||
colors,
|
||||
p.assetsPrefix(ctx),
|
||||
).SendDomainClaimed(notifyUser, origin, e.UserName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = p.commands.UserDomainClaimedSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) reducePasswordlessCodeRequested(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPasswordlessInitCodeRequestedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-EDtjd", "reduce.wrong.event.type %s", user.HumanPasswordlessInitCodeAddedType)
|
||||
}
|
||||
ctx := setNotificationContext(event.Aggregate())
|
||||
alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, map[string]interface{}{"id": e.ID}, user.HumanPasswordlessInitCodeSentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, p.userDataCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordlessRegistrationMessageType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
origin, err := p.origin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = types.SendEmail(
|
||||
ctx,
|
||||
string(template.Template),
|
||||
translator,
|
||||
notifyUser,
|
||||
p.getSMTPConfig,
|
||||
p.getFileSystemProvider,
|
||||
p.getLogProvider,
|
||||
colors,
|
||||
p.assetsPrefix(ctx),
|
||||
).SendPasswordlessRegistrationLink(notifyUser, origin, code, e.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = p.commands.HumanPasswordlessInitCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) reducePhoneCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanPhoneCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-He83g", "reduce.wrong.event.type %s", user.HumanPhoneCodeAddedType)
|
||||
}
|
||||
ctx := setNotificationContext(event.Aggregate())
|
||||
alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.UserV1PhoneCodeAddedType, user.UserV1PhoneCodeSentType,
|
||||
user.HumanPhoneCodeAddedType, user.HumanPhoneCodeSentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, p.userDataCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyPhoneMessageType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
origin, err := p.origin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = types.SendSMSTwilio(
|
||||
ctx,
|
||||
translator,
|
||||
notifyUser,
|
||||
p.getTwilioConfig,
|
||||
p.getFileSystemProvider,
|
||||
p.getLogProvider,
|
||||
colors,
|
||||
p.assetsPrefix(ctx),
|
||||
).SendPhoneVerificationCode(notifyUser, origin, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = p.commands.HumanPhoneVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event eventstore.Event, expiry time.Duration, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) {
|
||||
if event.CreationDate().Add(expiry).Before(time.Now().UTC()) {
|
||||
return true, nil
|
||||
}
|
||||
return p.checkIfAlreadyHandled(ctx, event, data, eventTypes...)
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) checkIfAlreadyHandled(ctx context.Context, event eventstore.Event, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) {
|
||||
events, err := p.es.Filter(
|
||||
ctx,
|
||||
eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
InstanceID(event.Aggregate().InstanceID).
|
||||
AddQuery().
|
||||
AggregateTypes(user.AggregateType).
|
||||
AggregateIDs(event.Aggregate().ID).
|
||||
SequenceGreater(event.Sequence()).
|
||||
EventTypes(eventTypes...).
|
||||
EventData(data).
|
||||
Builder(),
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return len(events) > 0, nil
|
||||
}
|
||||
func (p *notificationsProjection) getSMTPConfig(ctx context.Context) (*smtp.EmailConfig, error) {
|
||||
config, err := p.queries.SMTPConfigByAggregateID(ctx, authz.GetInstance(ctx).InstanceID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password, err := crypto.DecryptString(config.Password, p.smtpPasswordCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &smtp.EmailConfig{
|
||||
From: config.SenderAddress,
|
||||
FromName: config.SenderName,
|
||||
Tls: config.TLS,
|
||||
SMTP: smtp.SMTP{
|
||||
Host: config.Host,
|
||||
User: config.User,
|
||||
Password: password,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read iam twilio config
|
||||
func (p *notificationsProjection) getTwilioConfig(ctx context.Context) (*twilio.TwilioConfig, error) {
|
||||
active, err := query.NewSMSProviderStateQuery(domain.SMSConfigStateActive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := p.queries.SMSProviderConfig(ctx, active)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.TwilioConfig == nil {
|
||||
return nil, errors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound")
|
||||
}
|
||||
token, err := crypto.DecryptString(config.TwilioConfig.Token, p.smsTokenCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &twilio.TwilioConfig{
|
||||
SID: config.TwilioConfig.SID,
|
||||
Token: token,
|
||||
SenderNumber: config.TwilioConfig.SenderNumber,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read iam filesystem provider config
|
||||
func (p *notificationsProjection) getFileSystemProvider(ctx context.Context) (*fs.FSConfig, error) {
|
||||
config, err := p.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fs.FSConfig{
|
||||
Compact: config.Compact,
|
||||
Path: p.fileSystemPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read iam log provider config
|
||||
func (p *notificationsProjection) getLogProvider(ctx context.Context) (*log.LogConfig, error) {
|
||||
config, err := p.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeLog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &log.LogConfig{
|
||||
Compact: config.Compact,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) getTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) {
|
||||
translator, err := i18n.NewTranslator(p.statikDir, p.queries.GetDefaultLanguage(ctx), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allCustomTexts, err := p.queries.CustomTextListByTemplate(ctx, authz.GetInstance(ctx).InstanceID(), textType)
|
||||
if err != nil {
|
||||
return translator, nil
|
||||
}
|
||||
customTexts, err := p.queries.CustomTextListByTemplate(ctx, orgID, textType)
|
||||
if err != nil {
|
||||
return translator, nil
|
||||
}
|
||||
allCustomTexts.CustomTexts = append(allCustomTexts.CustomTexts, customTexts.CustomTexts...)
|
||||
|
||||
for _, text := range allCustomTexts.CustomTexts {
|
||||
msg := i18n.Message{
|
||||
ID: text.Template + "." + text.Key,
|
||||
Text: text.Text,
|
||||
}
|
||||
err = translator.AddMessages(text.Language, msg)
|
||||
logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "orgID", orgID, "messageType", textType, "messageID", msg.ID).
|
||||
OnError(err).
|
||||
Warn("could not add translation message")
|
||||
}
|
||||
return translator, nil
|
||||
}
|
||||
|
||||
func (p *notificationsProjection) origin(ctx context.Context) (string, error) {
|
||||
primary, err := query.NewInstanceDomainPrimarySearchQuery(true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
domains, err := p.queries.SearchInstanceDomains(ctx, &query.InstanceDomainSearchQueries{
|
||||
Queries: []query.SearchQuery{primary},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(domains.Domains) < 1 {
|
||||
return "", errors.ThrowInternal(nil, "NOTIF-Ef3r1", "Errors.Notification.NoDomain")
|
||||
}
|
||||
return http_utils.BuildHTTP(domains.Domains[0].Domain, p.externalPort, p.externalSecure), nil
|
||||
}
|
||||
|
||||
func setNotificationContext(event eventstore.Aggregate) context.Context {
|
||||
ctx := authz.WithInstanceID(context.Background(), event.InstanceID)
|
||||
return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: event.ResourceOwner})
|
||||
}
|
@@ -1,89 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
queryv1 "github.com/zitadel/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/view"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type Configs map[string]*Config
|
||||
|
||||
type Config struct {
|
||||
MinimumCycleDuration time.Duration
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
view *view.View
|
||||
bulkLimit uint64
|
||||
cycleDuration time.Duration
|
||||
errorCountUntilSkip uint64
|
||||
|
||||
es v1.Eventstore
|
||||
}
|
||||
|
||||
func (h *handler) Eventstore() v1.Eventstore {
|
||||
return h.es
|
||||
}
|
||||
|
||||
func Register(configs Configs,
|
||||
bulkLimit,
|
||||
errorCount uint64,
|
||||
view *view.View,
|
||||
es v1.Eventstore,
|
||||
command *command.Commands,
|
||||
queries *query.Queries,
|
||||
externalPort uint16,
|
||||
externalSecure bool,
|
||||
dir http.FileSystem,
|
||||
assetsPrefix,
|
||||
fileSystemPath string,
|
||||
userEncryption crypto.EncryptionAlgorithm,
|
||||
smtpEncryption crypto.EncryptionAlgorithm,
|
||||
smsEncryption crypto.EncryptionAlgorithm,
|
||||
) []queryv1.Handler {
|
||||
return []queryv1.Handler{
|
||||
newNotifyUser(
|
||||
handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es},
|
||||
queries,
|
||||
),
|
||||
newNotification(
|
||||
handler{view, bulkLimit, configs.cycleDuration("Notification"), errorCount, es},
|
||||
command,
|
||||
queries,
|
||||
externalPort,
|
||||
externalSecure,
|
||||
dir,
|
||||
assetsPrefix,
|
||||
fileSystemPath,
|
||||
userEncryption,
|
||||
smtpEncryption,
|
||||
smsEncryption,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (configs Configs) cycleDuration(viewModel string) time.Duration {
|
||||
c, ok := configs[viewModel]
|
||||
if !ok {
|
||||
return 1 * time.Minute
|
||||
}
|
||||
return c.MinimumCycleDuration
|
||||
}
|
||||
|
||||
func (h *handler) MinimumCycleDuration() time.Duration {
|
||||
return h.cycleDuration
|
||||
}
|
||||
|
||||
func (h *handler) LockDuration() time.Duration {
|
||||
return h.cycleDuration / 3
|
||||
}
|
||||
|
||||
func (h *handler) QueryLimit() uint64 {
|
||||
return h.bulkLimit
|
||||
}
|
@@ -1,637 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
queryv1 "github.com/zitadel/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
|
||||
"github.com/zitadel/zitadel/internal/notification/types"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
user_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"github.com/zitadel/zitadel/internal/user/repository/view"
|
||||
"github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
const (
|
||||
notificationTable = "notification.notifications"
|
||||
NotifyUserID = "NOTIFICATION"
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
handler
|
||||
command *command.Commands
|
||||
fileSystemPath string
|
||||
statikDir http.FileSystem
|
||||
subscription *v1.Subscription
|
||||
assetsPrefix string
|
||||
queries *query.Queries
|
||||
userDataCrypto crypto.EncryptionAlgorithm
|
||||
smtpPasswordCrypto crypto.EncryptionAlgorithm
|
||||
smsTokenCrypto crypto.EncryptionAlgorithm
|
||||
externalPort uint16
|
||||
externalSecure bool
|
||||
}
|
||||
|
||||
func newNotification(
|
||||
handler handler,
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
externalPort uint16,
|
||||
externalSecure bool,
|
||||
statikDir http.FileSystem,
|
||||
assetsPrefix,
|
||||
fileSystemPath string,
|
||||
userEncryption crypto.EncryptionAlgorithm,
|
||||
smtpEncryption crypto.EncryptionAlgorithm,
|
||||
smsEncryption crypto.EncryptionAlgorithm,
|
||||
) *Notification {
|
||||
h := &Notification{
|
||||
handler: handler,
|
||||
command: command,
|
||||
statikDir: statikDir,
|
||||
assetsPrefix: assetsPrefix,
|
||||
queries: query,
|
||||
userDataCrypto: userEncryption,
|
||||
smtpPasswordCrypto: smtpEncryption,
|
||||
smsTokenCrypto: smsEncryption,
|
||||
externalSecure: externalSecure,
|
||||
externalPort: externalPort,
|
||||
fileSystemPath: fileSystemPath,
|
||||
}
|
||||
|
||||
h.subscribe()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (k *Notification) subscribe() {
|
||||
k.subscription = k.es.Subscribe(k.AggregateTypes()...)
|
||||
go func() {
|
||||
for event := range k.subscription.Events {
|
||||
queryv1.ReduceEvent(k, event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (n *Notification) ViewModel() string {
|
||||
return notificationTable
|
||||
}
|
||||
|
||||
func (n *Notification) Subscription() *v1.Subscription {
|
||||
return n.subscription
|
||||
}
|
||||
|
||||
func (_ *Notification) AggregateTypes() []models.AggregateType {
|
||||
return []models.AggregateType{user_repo.AggregateType}
|
||||
}
|
||||
|
||||
func (n *Notification) CurrentSequence(instanceID string) (uint64, error) {
|
||||
sequence, err := n.view.GetLatestNotificationSequence(instanceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (n *Notification) EventQuery() (*models.SearchQuery, error) {
|
||||
sequences, err := n.view.GetLatestNotificationSequences()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := models.NewSearchQuery()
|
||||
instances := make([]string, 0)
|
||||
for _, sequence := range sequences {
|
||||
for _, instance := range instances {
|
||||
if sequence.InstanceID == instance {
|
||||
break
|
||||
}
|
||||
}
|
||||
instances = append(instances, sequence.InstanceID)
|
||||
query.AddQuery().
|
||||
AggregateTypeFilter(n.AggregateTypes()...).
|
||||
LatestSequenceFilter(sequence.CurrentSequence).
|
||||
InstanceIDFilter(sequence.InstanceID)
|
||||
}
|
||||
return query.AddQuery().
|
||||
AggregateTypeFilter(n.AggregateTypes()...).
|
||||
LatestSequenceFilter(0).
|
||||
ExcludedInstanceIDsFilter(instances...).
|
||||
SearchQuery(), nil
|
||||
}
|
||||
|
||||
func (n *Notification) Reduce(event *models.Event) (err error) {
|
||||
switch eventstore.EventType(event.Type) {
|
||||
case user_repo.UserV1InitialCodeAddedType,
|
||||
user_repo.HumanInitialCodeAddedType:
|
||||
err = n.handleInitUserCode(event)
|
||||
case user_repo.UserV1EmailCodeAddedType,
|
||||
user_repo.HumanEmailCodeAddedType:
|
||||
err = n.handleEmailVerificationCode(event)
|
||||
case user_repo.UserV1PhoneCodeAddedType,
|
||||
user_repo.HumanPhoneCodeAddedType:
|
||||
err = n.handlePhoneVerificationCode(event)
|
||||
case user_repo.UserV1PasswordCodeAddedType,
|
||||
user_repo.HumanPasswordCodeAddedType:
|
||||
err = n.handlePasswordCode(event)
|
||||
case user_repo.UserDomainClaimedType:
|
||||
err = n.handleDomainClaimed(event)
|
||||
case user_repo.HumanPasswordlessInitCodeRequestedType:
|
||||
err = n.handlePasswordlessRegistrationLink(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.view.ProcessedNotificationSequence(event)
|
||||
}
|
||||
|
||||
func (n *Notification) handleInitUserCode(event *models.Event) (err error) {
|
||||
initCode := new(es_model.InitUserCode)
|
||||
if err := initCode.SetData(event); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, initCode.Expiry,
|
||||
user_repo.UserV1InitialCodeAddedType, user_repo.UserV1InitialCodeSentType,
|
||||
user_repo.HumanInitialCodeAddedType, user_repo.HumanInitialCodeSentType)
|
||||
if err != nil || alreadyHandled {
|
||||
return err
|
||||
}
|
||||
colors, err := n.getLabelPolicy(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := n.getMailTemplate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Sequence < event.Sequence {
|
||||
if err = n.verifyLatestUser(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if user.Sequence == 0 {
|
||||
return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found")
|
||||
}
|
||||
|
||||
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.InitCodeMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
origin, err := n.origin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendUserInitCode(ctx, string(template.Template), translator, user, initCode, n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.command.HumanInitCodeSent(ctx, event.ResourceOwner, event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handlePasswordCode(event *models.Event) (err error) {
|
||||
pwCode := new(es_model.PasswordCode)
|
||||
if err := pwCode.SetData(event); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, pwCode.Expiry,
|
||||
user_repo.UserV1PasswordCodeAddedType, user_repo.UserV1PasswordCodeSentType,
|
||||
user_repo.HumanPasswordCodeAddedType, user_repo.HumanPasswordCodeSentType)
|
||||
if err != nil || alreadyHandled {
|
||||
return err
|
||||
}
|
||||
colors, err := n.getLabelPolicy(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := n.getMailTemplate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Sequence < event.Sequence {
|
||||
if err = n.verifyLatestUser(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if user.Sequence == 0 {
|
||||
return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found")
|
||||
}
|
||||
|
||||
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.PasswordResetMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
origin, err := n.origin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendPasswordCode(ctx, string(template.Template), translator, user, pwCode, n.getSMTPConfig, n.getTwilioConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.command.PasswordCodeSent(ctx, event.ResourceOwner, event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handleEmailVerificationCode(event *models.Event) (err error) {
|
||||
emailCode := new(es_model.EmailCode)
|
||||
if err := emailCode.SetData(event); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, emailCode.Expiry,
|
||||
user_repo.UserV1EmailCodeAddedType, user_repo.UserV1EmailCodeSentType,
|
||||
user_repo.HumanEmailCodeAddedType, user_repo.HumanEmailCodeSentType)
|
||||
if err != nil || alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
colors, err := n.getLabelPolicy(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := n.getMailTemplate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
if user.Sequence < event.Sequence {
|
||||
if err = n.verifyLatestUser(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if user.Sequence == 0 {
|
||||
return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found")
|
||||
}
|
||||
|
||||
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.VerifyEmailMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
origin, err := n.origin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendEmailVerificationCode(ctx, string(template.Template), translator, user, emailCode, n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.command.HumanEmailVerificationCodeSent(ctx, event.ResourceOwner, event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handlePhoneVerificationCode(event *models.Event) (err error) {
|
||||
phoneCode := new(es_model.PhoneCode)
|
||||
if err := phoneCode.SetData(event); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, phoneCode.Expiry,
|
||||
user_repo.UserV1PhoneCodeAddedType, user_repo.UserV1PhoneCodeSentType,
|
||||
user_repo.HumanPhoneCodeAddedType, user_repo.HumanPhoneCodeSentType)
|
||||
if err != nil || alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Sequence < event.Sequence {
|
||||
if err = n.verifyLatestUser(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if user.Sequence == 0 {
|
||||
return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found")
|
||||
}
|
||||
|
||||
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.VerifyPhoneMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendPhoneVerificationCode(ctx, translator, user, phoneCode, n.getTwilioConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.command.HumanPhoneVerificationCodeSent(ctx, event.ResourceOwner, event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handleDomainClaimed(event *models.Event) (err error) {
|
||||
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
|
||||
alreadyHandled, err := n.checkIfAlreadyHandled(ctx, event.AggregateID, event.InstanceID, event.Sequence, user_repo.UserDomainClaimedType, user_repo.UserDomainClaimedSentType)
|
||||
if err != nil || alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
data := make(map[string]string)
|
||||
if err := json.Unmarshal(event.Data, &data); err != nil {
|
||||
logging.Log("HANDLE-Gghq2").WithError(err).Error("could not unmarshal event data")
|
||||
return errors.ThrowInternal(err, "HANDLE-7hgj3", "could not unmarshal event")
|
||||
}
|
||||
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.LastEmail == "" {
|
||||
return nil
|
||||
}
|
||||
colors, err := n.getLabelPolicy(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := n.getMailTemplate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.DomainClaimedMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
origin, err := n.origin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendDomainClaimed(ctx, string(template.Template), translator, user, data["userName"], n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, colors, n.assetsPrefix, origin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.command.UserDomainClaimedSent(ctx, event.ResourceOwner, event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handlePasswordlessRegistrationLink(event *models.Event) (err error) {
|
||||
addedEvent := new(user_repo.HumanPasswordlessInitCodeRequestedEvent)
|
||||
if err := json.Unmarshal(event.Data, addedEvent); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
|
||||
events, err := n.getUserEvents(ctx, event.AggregateID, event.InstanceID, event.Sequence)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range events {
|
||||
if eventstore.EventType(e.Type) == user_repo.HumanPasswordlessInitCodeSentType {
|
||||
sentEvent := new(user_repo.HumanPasswordlessInitCodeSentEvent)
|
||||
if err := json.Unmarshal(e.Data, sentEvent); err != nil {
|
||||
return err
|
||||
}
|
||||
if sentEvent.ID == addedEvent.ID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
colors, err := n.getLabelPolicy(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template, err := n.getMailTemplate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.PasswordlessRegistrationMessageType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
origin, err := n.origin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendPasswordlessRegistrationLink(ctx, string(template.Template), translator, user, addedEvent, n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.command.HumanPasswordlessInitCodeSent(ctx, event.AggregateID, event.ResourceOwner, addedEvent.ID)
|
||||
}
|
||||
|
||||
func (n *Notification) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event *models.Event, expiry time.Duration, eventTypes ...eventstore.EventType) (bool, error) {
|
||||
if event.CreationDate.Add(expiry).Before(time.Now().UTC()) {
|
||||
return true, nil
|
||||
}
|
||||
return n.checkIfAlreadyHandled(ctx, event.AggregateID, event.InstanceID, event.Sequence, eventTypes...)
|
||||
}
|
||||
|
||||
func (n *Notification) checkIfAlreadyHandled(ctx context.Context, userID, instanceID string, sequence uint64, eventTypes ...eventstore.EventType) (bool, error) {
|
||||
events, err := n.getUserEvents(ctx, userID, instanceID, sequence)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, event := range events {
|
||||
for _, eventType := range eventTypes {
|
||||
if eventstore.EventType(event.Type) == eventType {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (n *Notification) getUserEvents(ctx context.Context, userID, instanceID string, sequence uint64) ([]*models.Event, error) {
|
||||
query, err := view.UserByIDQuery(userID, instanceID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return n.es.FilterEvents(ctx, query)
|
||||
}
|
||||
|
||||
func (n *Notification) OnError(event *models.Event, err error) error {
|
||||
logging.WithFields("id", event.AggregateID, "sequence", event.Sequence).WithError(err).Warn("something went wrong in notification handler")
|
||||
return spooler.HandleError(event, err, n.view.GetLatestNotificationFailedEvent, n.view.ProcessedNotificationFailedEvent, n.view.ProcessedNotificationSequence, n.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func (n *Notification) OnSuccess() error {
|
||||
return spooler.HandleSuccess(n.view.UpdateNotificationSpoolerRunTimestamp)
|
||||
}
|
||||
|
||||
func getSetNotifyContextData(instanceID, orgID string) context.Context {
|
||||
ctx := authz.WithInstanceID(context.Background(), instanceID)
|
||||
return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: orgID})
|
||||
}
|
||||
|
||||
// Read organization specific colors
|
||||
func (n *Notification) getLabelPolicy(ctx context.Context) (*query.LabelPolicy, error) {
|
||||
return n.queries.ActiveLabelPolicyByOrg(ctx, authz.GetCtxData(ctx).OrgID)
|
||||
}
|
||||
|
||||
// Read organization specific template
|
||||
func (n *Notification) getMailTemplate(ctx context.Context) (*query.MailTemplate, error) {
|
||||
return n.queries.MailTemplateByOrg(ctx, authz.GetCtxData(ctx).OrgID)
|
||||
}
|
||||
|
||||
// Read iam smtp config
|
||||
func (n *Notification) getSMTPConfig(ctx context.Context) (*smtp.EmailConfig, error) {
|
||||
config, err := n.queries.SMTPConfigByAggregateID(ctx, authz.GetInstance(ctx).InstanceID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password, err := crypto.Decrypt(config.Password, n.smtpPasswordCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &smtp.EmailConfig{
|
||||
From: config.SenderAddress,
|
||||
FromName: config.SenderName,
|
||||
Tls: config.TLS,
|
||||
SMTP: smtp.SMTP{
|
||||
Host: config.Host,
|
||||
User: config.User,
|
||||
Password: string(password),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read iam twilio config
|
||||
func (n *Notification) getTwilioConfig(ctx context.Context) (*twilio.TwilioConfig, error) {
|
||||
active, err := query.NewSMSProviderStateQuery(domain.SMSConfigStateActive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := n.queries.SMSProviderConfig(ctx, active)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.TwilioConfig == nil {
|
||||
return nil, errors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound")
|
||||
}
|
||||
token, err := crypto.Decrypt(config.TwilioConfig.Token, n.smsTokenCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &twilio.TwilioConfig{
|
||||
SID: config.TwilioConfig.SID,
|
||||
Token: string(token),
|
||||
SenderNumber: config.TwilioConfig.SenderNumber,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read iam filesystem provider config
|
||||
func (n *Notification) getFileSystemProvider(ctx context.Context) (*fs.FSConfig, error) {
|
||||
config, err := n.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fs.FSConfig{
|
||||
Compact: config.Compact,
|
||||
Path: n.fileSystemPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read iam log provider config
|
||||
func (n *Notification) getLogProvider(ctx context.Context) (*log.LogConfig, error) {
|
||||
config, err := n.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeLog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &log.LogConfig{
|
||||
Compact: config.Compact,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (n *Notification) getTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) {
|
||||
translator, err := i18n.NewTranslator(n.statikDir, n.queries.GetDefaultLanguage(ctx), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allCustomTexts, err := n.queries.CustomTextListByTemplate(ctx, authz.GetInstance(ctx).InstanceID(), textType)
|
||||
if err != nil {
|
||||
return translator, nil
|
||||
}
|
||||
customTexts, err := n.queries.CustomTextListByTemplate(ctx, orgID, textType)
|
||||
if err != nil {
|
||||
return translator, nil
|
||||
}
|
||||
allCustomTexts.CustomTexts = append(allCustomTexts.CustomTexts, customTexts.CustomTexts...)
|
||||
|
||||
for _, text := range allCustomTexts.CustomTexts {
|
||||
msg := i18n.Message{
|
||||
ID: text.Template + "." + text.Key,
|
||||
Text: text.Text,
|
||||
}
|
||||
translator.AddMessages(text.Language, msg)
|
||||
}
|
||||
return translator, nil
|
||||
}
|
||||
|
||||
func (n *Notification) getUserByID(userID, instanceID string) (*model.NotifyUser, error) {
|
||||
return n.view.NotifyUserByID(userID, instanceID)
|
||||
}
|
||||
|
||||
func (n *Notification) origin(ctx context.Context) (string, error) {
|
||||
primary, err := query.NewInstanceDomainPrimarySearchQuery(true)
|
||||
domains, err := n.queries.SearchInstanceDomains(ctx, &query.InstanceDomainSearchQueries{
|
||||
Queries: []query.SearchQuery{primary},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(domains.Domains) < 1 {
|
||||
return "", errors.ThrowInternal(nil, "NOTIF-Ef3r1", "Errors.Notification.NoDomain")
|
||||
}
|
||||
return http_utils.BuildHTTP(domains.Domains[0].Domain, n.externalPort, n.externalSecure), nil
|
||||
}
|
||||
|
||||
func (n *Notification) verifyLatestUser(ctx context.Context, user *model.NotifyUser) error {
|
||||
events, err := n.getUserEvents(ctx, user.ID, user.InstanceID, user.Sequence)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, event := range events {
|
||||
if err = user.AppendEvent(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -1,278 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/query"
|
||||
es_sdk "github.com/zitadel/zitadel/internal/eventstore/v1/sdk"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
|
||||
org_model "github.com/zitadel/zitadel/internal/org/model"
|
||||
org_es_model "github.com/zitadel/zitadel/internal/org/repository/eventsourcing/model"
|
||||
org_view "github.com/zitadel/zitadel/internal/org/repository/view"
|
||||
query2 "github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
const (
|
||||
userTable = "notification.notify_users"
|
||||
)
|
||||
|
||||
type NotifyUser struct {
|
||||
handler
|
||||
subscription *v1.Subscription
|
||||
queries *query2.Queries
|
||||
}
|
||||
|
||||
func newNotifyUser(
|
||||
handler handler,
|
||||
queries *query2.Queries,
|
||||
) *NotifyUser {
|
||||
h := &NotifyUser{
|
||||
handler: handler,
|
||||
queries: queries,
|
||||
}
|
||||
|
||||
h.subscribe()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (k *NotifyUser) subscribe() {
|
||||
k.subscription = k.es.Subscribe(k.AggregateTypes()...)
|
||||
go func() {
|
||||
for event := range k.subscription.Events {
|
||||
query.ReduceEvent(k, event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *NotifyUser) ViewModel() string {
|
||||
return userTable
|
||||
}
|
||||
|
||||
func (p *NotifyUser) Subscription() *v1.Subscription {
|
||||
return p.subscription
|
||||
}
|
||||
|
||||
func (_ *NotifyUser) AggregateTypes() []es_models.AggregateType {
|
||||
return []es_models.AggregateType{user.AggregateType, org.AggregateType}
|
||||
}
|
||||
|
||||
func (p *NotifyUser) CurrentSequence(instanceID string) (uint64, error) {
|
||||
sequence, err := p.view.GetLatestNotifyUserSequence(instanceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (p *NotifyUser) EventQuery() (*es_models.SearchQuery, error) {
|
||||
sequences, err := p.view.GetLatestNotifyUserSequences()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := es_models.NewSearchQuery()
|
||||
instances := make([]string, 0)
|
||||
for _, sequence := range sequences {
|
||||
for _, instance := range instances {
|
||||
if sequence.InstanceID == instance {
|
||||
break
|
||||
}
|
||||
}
|
||||
instances = append(instances, sequence.InstanceID)
|
||||
query.AddQuery().
|
||||
AggregateTypeFilter(p.AggregateTypes()...).
|
||||
LatestSequenceFilter(sequence.CurrentSequence).
|
||||
InstanceIDFilter(sequence.InstanceID)
|
||||
}
|
||||
return query.AddQuery().
|
||||
AggregateTypeFilter(p.AggregateTypes()...).
|
||||
LatestSequenceFilter(0).
|
||||
ExcludedInstanceIDsFilter(instances...).
|
||||
SearchQuery(), nil
|
||||
}
|
||||
|
||||
func (u *NotifyUser) Reduce(event *es_models.Event) (err error) {
|
||||
switch event.AggregateType {
|
||||
case user.AggregateType:
|
||||
return u.ProcessUser(event)
|
||||
case org.AggregateType:
|
||||
return u.ProcessOrg(event)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u *NotifyUser) ProcessUser(event *es_models.Event) (err error) {
|
||||
notifyUser := new(view_model.NotifyUser)
|
||||
switch eventstore.EventType(event.Type) {
|
||||
case user.UserV1AddedType,
|
||||
user.UserV1RegisteredType,
|
||||
user.HumanRegisteredType,
|
||||
user.HumanAddedType,
|
||||
user.MachineAddedEventType:
|
||||
err = notifyUser.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = u.fillLoginNames(notifyUser)
|
||||
case user.UserV1ProfileChangedType,
|
||||
user.UserV1EmailChangedType,
|
||||
user.UserV1EmailVerifiedType,
|
||||
user.UserV1PhoneChangedType,
|
||||
user.UserV1PhoneVerifiedType,
|
||||
user.UserV1PhoneRemovedType,
|
||||
user.HumanProfileChangedType,
|
||||
user.HumanEmailChangedType,
|
||||
user.HumanEmailVerifiedType,
|
||||
user.HumanPhoneChangedType,
|
||||
user.HumanPhoneVerifiedType,
|
||||
user.HumanPhoneRemovedType,
|
||||
user.MachineChangedEventType:
|
||||
notifyUser, err = u.view.NotifyUserByID(event.AggregateID, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = notifyUser.AppendEvent(event)
|
||||
case user.UserDomainClaimedType,
|
||||
user.UserUserNameChangedType:
|
||||
notifyUser, err = u.view.NotifyUserByID(event.AggregateID, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = notifyUser.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = u.fillLoginNames(notifyUser)
|
||||
case user.UserRemovedType:
|
||||
return u.view.DeleteNotifyUser(event.AggregateID, event.InstanceID, event)
|
||||
default:
|
||||
return u.view.ProcessedNotifyUserSequence(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return u.view.PutNotifyUser(notifyUser, event)
|
||||
}
|
||||
|
||||
func (u *NotifyUser) ProcessOrg(event *es_models.Event) (err error) {
|
||||
switch eventstore.EventType(event.Type) {
|
||||
case org.OrgDomainVerifiedEventType,
|
||||
org.OrgDomainRemovedEventType,
|
||||
org.DomainPolicyAddedEventType,
|
||||
org.DomainPolicyChangedEventType,
|
||||
org.DomainPolicyRemovedEventType:
|
||||
return u.fillLoginNamesOnOrgUsers(event)
|
||||
case org.OrgDomainPrimarySetEventType:
|
||||
return u.fillPreferredLoginNamesOnOrgUsers(event)
|
||||
default:
|
||||
return u.view.ProcessedNotifyUserSequence(event)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *NotifyUser) fillLoginNamesOnOrgUsers(event *es_models.Event) error {
|
||||
userLoginMustBeDomain, _, domains, err := u.loginNameInformation(context.Background(), event.ResourceOwner, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
users, err := u.view.NotifyUsersByOrgID(event.AggregateID, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, user := range users {
|
||||
user.SetLoginNames(userLoginMustBeDomain, domains)
|
||||
err := u.view.PutNotifyUser(user, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return u.view.ProcessedNotifyUserSequence(event)
|
||||
}
|
||||
|
||||
func (u *NotifyUser) fillPreferredLoginNamesOnOrgUsers(event *es_models.Event) error {
|
||||
userLoginMustBeDomain, primaryDomain, _, err := u.loginNameInformation(context.Background(), event.ResourceOwner, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !userLoginMustBeDomain {
|
||||
return nil
|
||||
}
|
||||
users, err := u.view.NotifyUsersByOrgID(event.AggregateID, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, user := range users {
|
||||
user.PreferredLoginName = user.GenerateLoginName(primaryDomain, userLoginMustBeDomain)
|
||||
err := u.view.PutNotifyUser(user, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *NotifyUser) fillLoginNames(user *view_model.NotifyUser) (err error) {
|
||||
userLoginMustBeDomain, primaryDomain, domains, err := u.loginNameInformation(context.Background(), user.ResourceOwner, user.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.SetLoginNames(userLoginMustBeDomain, domains)
|
||||
user.PreferredLoginName = user.GenerateLoginName(primaryDomain, userLoginMustBeDomain)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *NotifyUser) OnError(event *es_models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-9spwf", "id", event.AggregateID).WithError(err).Warn("something went wrong in notify user handler")
|
||||
return spooler.HandleError(event, err, p.view.GetLatestNotifyUserFailedEvent, p.view.ProcessedNotifyUserFailedEvent, p.view.ProcessedNotifyUserSequence, p.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func (u *NotifyUser) OnSuccess() error {
|
||||
return spooler.HandleSuccess(u.view.UpdateNotifyUserSpoolerRunTimestamp)
|
||||
}
|
||||
|
||||
func (u *NotifyUser) getOrgByID(ctx context.Context, orgID, instanceID string) (*org_model.Org, error) {
|
||||
query, err := org_view.OrgByIDQuery(orgID, instanceID, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
esOrg := &org_es_model.Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: orgID,
|
||||
},
|
||||
}
|
||||
err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, query)
|
||||
if err != nil && !caos_errs.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
if esOrg.Sequence == 0 {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "EVENT-kVLb2", "Errors.Org.NotFound")
|
||||
}
|
||||
|
||||
return org_es_model.OrgToModel(esOrg), nil
|
||||
}
|
||||
|
||||
func (u *NotifyUser) loginNameInformation(ctx context.Context, orgID, instanceID string) (userLoginMustBeDomain bool, primaryDomain string, domains []*org_model.OrgDomain, err error) {
|
||||
org, err := u.getOrgByID(ctx, orgID, instanceID)
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
}
|
||||
if org.DomainPolicy == nil {
|
||||
policy, err := u.queries.DefaultDomainPolicy(authz.WithInstanceID(ctx, org.InstanceID))
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
}
|
||||
userLoginMustBeDomain = policy.UserLoginMustBeDomain
|
||||
}
|
||||
return userLoginMustBeDomain, org.GetPrimaryDomain().Domain, org.Domains, nil
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
es_spol "github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
|
||||
"github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/spooler"
|
||||
noti_view "github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/view"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Spooler spooler.SpoolerConfig
|
||||
}
|
||||
|
||||
type EsRepository struct {
|
||||
spooler *es_spol.Spooler
|
||||
}
|
||||
|
||||
func Start(conf Config,
|
||||
dir http.FileSystem,
|
||||
externalPort uint16,
|
||||
externalSecure bool,
|
||||
command *command.Commands,
|
||||
queries *query.Queries,
|
||||
dbClient *sql.DB,
|
||||
assetsPrefix,
|
||||
fileSystemPath string,
|
||||
userEncryption crypto.EncryptionAlgorithm,
|
||||
smtpEncryption crypto.EncryptionAlgorithm,
|
||||
smsEncryption crypto.EncryptionAlgorithm,
|
||||
) (*EsRepository, error) {
|
||||
es, err := v1.Start(dbClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
view, err := noti_view.StartView(dbClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spool := spooler.StartSpooler(conf.Spooler, es, view, dbClient, command, queries, externalPort, externalSecure, dir, assetsPrefix, fileSystemPath, userEncryption, smtpEncryption, smsEncryption)
|
||||
|
||||
return &EsRepository{
|
||||
spool,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *EsRepository) Health() error {
|
||||
return nil
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
es_locker "github.com/zitadel/zitadel/internal/eventstore/v1/locker"
|
||||
)
|
||||
|
||||
const (
|
||||
lockTable = "notification.locks"
|
||||
)
|
||||
|
||||
type locker struct {
|
||||
dbClient *sql.DB
|
||||
}
|
||||
|
||||
func (l *locker) Renew(lockerID, viewModel, instanceID string, waitTime time.Duration) error {
|
||||
return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, instanceID, waitTime)
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
|
||||
"github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/handler"
|
||||
"github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/view"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type SpoolerConfig struct {
|
||||
BulkLimit uint64
|
||||
FailureCountUntilSkip uint64
|
||||
ConcurrentWorkers int
|
||||
Handlers handler.Configs
|
||||
}
|
||||
|
||||
func StartSpooler(c SpoolerConfig,
|
||||
es v1.Eventstore,
|
||||
view *view.View,
|
||||
sql *sql.DB,
|
||||
command *command.Commands,
|
||||
queries *query.Queries,
|
||||
externalPort uint16,
|
||||
externalSecure bool,
|
||||
dir http.FileSystem,
|
||||
assetsPrefix,
|
||||
fileSystemPath string,
|
||||
userEncryption crypto.EncryptionAlgorithm,
|
||||
smtpEncryption crypto.EncryptionAlgorithm,
|
||||
smsEncryption crypto.EncryptionAlgorithm,
|
||||
) *spooler.Spooler {
|
||||
spoolerConfig := spooler.Config{
|
||||
Eventstore: es,
|
||||
Locker: &locker{dbClient: sql},
|
||||
ConcurrentWorkers: c.ConcurrentWorkers,
|
||||
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, command, queries, externalPort, externalSecure, dir, assetsPrefix, fileSystemPath, userEncryption, smtpEncryption, smsEncryption),
|
||||
}
|
||||
spool := spoolerConfig.New()
|
||||
spool.Start()
|
||||
return spool
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
errTable = "notification.failed_events"
|
||||
)
|
||||
|
||||
func (v *View) saveFailedEvent(failedEvent *repository.FailedEvent) error {
|
||||
return repository.SaveFailedEvent(v.Db, errTable, failedEvent)
|
||||
}
|
||||
|
||||
func (v *View) latestFailedEvent(viewName, instanceID string, sequence uint64) (*repository.FailedEvent, error) {
|
||||
return repository.LatestFailedEvent(v.Db, errTable, viewName, instanceID, sequence)
|
||||
}
|
@@ -1,34 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
notificationTable = "notification.notifications"
|
||||
)
|
||||
|
||||
func (v *View) GetLatestNotificationSequence(instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(notificationTable, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotificationSequences() ([]*repository.CurrentSequence, error) {
|
||||
return v.latestSequences(notificationTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotificationSequence(event *models.Event) error {
|
||||
return v.saveCurrentSequence(notificationTable, event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateNotificationSpoolerRunTimestamp() error {
|
||||
return v.updateSpoolerRunSequence(notificationTable)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotificationFailedEvent(sequence uint64, instanceID string) (*repository.FailedEvent, error) {
|
||||
return v.latestFailedEvent(notificationTable, instanceID, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotificationFailedEvent(failedEvent *repository.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/user/repository/view"
|
||||
"github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
notifyUserTable = "notification.notify_users"
|
||||
)
|
||||
|
||||
func (v *View) NotifyUserByID(userID, instanceID string) (*model.NotifyUser, error) {
|
||||
return view.NotifyUserByID(v.Db, notifyUserTable, userID, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) PutNotifyUser(user *model.NotifyUser, event *models.Event) error {
|
||||
err := view.PutNotifyUser(v.Db, notifyUserTable, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedNotifyUserSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) NotifyUsersByOrgID(orgID, instanceID string) ([]*model.NotifyUser, error) {
|
||||
return view.NotifyUsersByOrgID(v.Db, notifyUserTable, orgID, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) DeleteNotifyUser(userID, instanceID string, event *models.Event) error {
|
||||
err := view.DeleteNotifyUser(v.Db, notifyUserTable, userID, instanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedNotifyUserSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotifyUserSequence(instanceID string) (*repository.CurrentSequence, error) {
|
||||
return v.latestSequence(notifyUserTable, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotifyUserSequences() ([]*repository.CurrentSequence, error) {
|
||||
return v.latestSequences(notifyUserTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotifyUserSequence(event *models.Event) error {
|
||||
return v.saveCurrentSequence(notifyUserTable, event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateNotifyUserSpoolerRunTimestamp() error {
|
||||
return v.updateSpoolerRunSequence(notifyUserTable)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotifyUserFailedEvent(sequence uint64, instanceID string) (*repository.FailedEvent, error) {
|
||||
return v.latestFailedEvent(notifyUserTable, instanceID, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotifyUserFailedEvent(failedEvent *repository.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
sequencesTable = "notification.current_sequences"
|
||||
)
|
||||
|
||||
func (v *View) saveCurrentSequence(viewName string, event *models.Event) error {
|
||||
return repository.SaveCurrentSequence(v.Db, sequencesTable, viewName, event.InstanceID, event.Sequence, event.CreationDate)
|
||||
}
|
||||
|
||||
func (v *View) latestSequence(viewName, instanceID string) (*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequence(v.Db, sequencesTable, viewName, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) latestSequences(viewName string) ([]*repository.CurrentSequence, error) {
|
||||
return repository.LatestSequences(v.Db, sequencesTable, viewName)
|
||||
}
|
||||
|
||||
func (v *View) updateSpoolerRunSequence(viewName string) error {
|
||||
currentSequences, err := repository.LatestSequences(v.Db, sequencesTable, viewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, currentSequence := range currentSequences {
|
||||
if currentSequence.ViewName == "" {
|
||||
currentSequence.ViewName = viewName
|
||||
}
|
||||
currentSequence.LastSuccessfulSpoolerRun = time.Now()
|
||||
}
|
||||
return repository.UpdateCurrentSequences(v.Db, sequencesTable, currentSequences)
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
type View struct {
|
||||
Db *gorm.DB
|
||||
}
|
||||
|
||||
func StartView(sqlClient *sql.DB) (*View, error) {
|
||||
gorm, err := gorm.Open("postgres", sqlClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &View{
|
||||
Db: gorm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *View) Health() (err error) {
|
||||
return v.Db.DB().Ping()
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
package repository
|
||||
|
||||
type Repository interface {
|
||||
Health() error
|
||||
}
|
@@ -21,7 +21,7 @@ type TemplateData struct {
|
||||
Subject string
|
||||
Greeting string
|
||||
Text string
|
||||
Href string
|
||||
URL string
|
||||
ButtonText string
|
||||
PrimaryColor string
|
||||
BackgroundColor string
|
||||
|
@@ -1,38 +1,17 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/templates"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type DomainClaimedData struct {
|
||||
templates.TemplateData
|
||||
URL string
|
||||
}
|
||||
|
||||
func SendDomainClaimed(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, username string, emailConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), colors *query.LabelPolicy, assetsPrefix string, origin string) error {
|
||||
func (notify Notify) SendDomainClaimed(user *query.NotifyUser, origin, username string) error {
|
||||
url := login.LoginLink(origin, user.ResourceOwner)
|
||||
var args = mapNotifyUserToArgs(user)
|
||||
args := make(map[string]interface{})
|
||||
args["TempUsername"] = username
|
||||
args["Domain"] = strings.Split(user.LastEmail, "@")[1]
|
||||
|
||||
domainClaimedData := &DomainClaimedData{
|
||||
TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.DomainClaimedMessageType, user.PreferredLanguage, colors),
|
||||
URL: url,
|
||||
}
|
||||
template, err := templates.GetParsedTemplate(mailhtml, domainClaimedData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(ctx, user, domainClaimedData.Subject, template, emailConfig, getFileSystemProvider, getLogProvider, true)
|
||||
return notify(url, args, domain.DomainClaimedMessageType, true)
|
||||
}
|
||||
|
@@ -1,43 +1,14 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/templates"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type EmailVerificationCodeData struct {
|
||||
templates.TemplateData
|
||||
URL string
|
||||
}
|
||||
|
||||
func SendEmailVerificationCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.EmailCode, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string, origin string) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := login.MailVerificationLink(origin, user.ID, codeString, user.ResourceOwner)
|
||||
var args = mapNotifyUserToArgs(user)
|
||||
args["Code"] = codeString
|
||||
|
||||
emailCodeData := &EmailVerificationCodeData{
|
||||
TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.VerifyEmailMessageType, user.PreferredLanguage, colors),
|
||||
URL: url,
|
||||
}
|
||||
|
||||
template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(ctx, user, emailCodeData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true)
|
||||
func (notify Notify) SendEmailVerificationCode(user *query.NotifyUser, origin, code string) error {
|
||||
url := login.MailVerificationLink(origin, user.ID, code, user.ResourceOwner)
|
||||
args := make(map[string]interface{})
|
||||
args["Code"] = code
|
||||
return notify(url, args, domain.VerifyEmailMessageType, true)
|
||||
}
|
||||
|
@@ -1,49 +1,14 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/templates"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type InitCodeEmailData struct {
|
||||
templates.TemplateData
|
||||
URL string
|
||||
}
|
||||
|
||||
type UrlData struct {
|
||||
UserID string
|
||||
Code string
|
||||
PasswordSet bool
|
||||
OrgID string
|
||||
}
|
||||
|
||||
func SendUserInitCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.InitUserCode, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix, origin string) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := login.InitUserLink(origin, user.ID, codeString, user.ResourceOwner, user.PasswordSet)
|
||||
var args = mapNotifyUserToArgs(user)
|
||||
args["Code"] = codeString
|
||||
|
||||
initCodeData := &InitCodeEmailData{
|
||||
TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.InitCodeMessageType, user.PreferredLanguage, colors),
|
||||
URL: url,
|
||||
}
|
||||
template, err := templates.GetParsedTemplate(mailhtml, initCodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(ctx, user, initCodeData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true)
|
||||
func (notify Notify) SendUserInitCode(user *query.NotifyUser, origin, code string) error {
|
||||
url := login.InitUserLink(origin, user.ID, code, user.ResourceOwner, user.PasswordSet)
|
||||
args := make(map[string]interface{})
|
||||
args["Code"] = code
|
||||
return notify(url, args, domain.InitCodeMessageType, true)
|
||||
}
|
||||
|
73
internal/notification/types/notification.go
Normal file
73
internal/notification/types/notification.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
|
||||
"github.com/zitadel/zitadel/internal/notification/templates"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type Notify func(
|
||||
url string,
|
||||
args map[string]interface{},
|
||||
messageType string,
|
||||
allowUnverifiedNotificationChannel bool,
|
||||
) error
|
||||
|
||||
func SendEmail(
|
||||
ctx context.Context,
|
||||
mailhtml string,
|
||||
translator *i18n.Translator,
|
||||
user *query.NotifyUser,
|
||||
emailConfig func(ctx context.Context) (*smtp.EmailConfig, error),
|
||||
getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error),
|
||||
getLogProvider func(ctx context.Context) (*log.LogConfig, error),
|
||||
colors *query.LabelPolicy,
|
||||
assetsPrefix string,
|
||||
) Notify {
|
||||
return func(
|
||||
url string,
|
||||
args map[string]interface{},
|
||||
messageType string,
|
||||
allowUnverifiedNotificationChannel bool,
|
||||
) error {
|
||||
args = mapNotifyUserToArgs(user, args)
|
||||
data := GetTemplateData(translator, args, assetsPrefix, url, messageType, user.PreferredLanguage.String(), colors)
|
||||
template, err := templates.GetParsedTemplate(mailhtml, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(ctx, user, data.Subject, template, emailConfig, getFileSystemProvider, getLogProvider, allowUnverifiedNotificationChannel)
|
||||
}
|
||||
}
|
||||
|
||||
func SendSMSTwilio(
|
||||
ctx context.Context,
|
||||
translator *i18n.Translator,
|
||||
user *query.NotifyUser,
|
||||
twilioConfig func(ctx context.Context) (*twilio.TwilioConfig, error),
|
||||
getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error),
|
||||
getLogProvider func(ctx context.Context) (*log.LogConfig, error),
|
||||
colors *query.LabelPolicy,
|
||||
assetsPrefix string,
|
||||
) Notify {
|
||||
return func(
|
||||
url string,
|
||||
args map[string]interface{},
|
||||
messageType string,
|
||||
allowUnverifiedNotificationChannel bool,
|
||||
) error {
|
||||
args = mapNotifyUserToArgs(user, args)
|
||||
data := GetTemplateData(translator, args, assetsPrefix, url, messageType, user.PreferredLanguage.String(), colors)
|
||||
return generateSms(ctx, user, data.Text, twilioConfig, getFileSystemProvider, getLogProvider, allowUnverifiedNotificationChannel)
|
||||
}
|
||||
}
|
||||
|
||||
func externalLink(origin string) string {
|
||||
return origin + "/ui/login"
|
||||
}
|
@@ -1,51 +1,14 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
|
||||
"github.com/zitadel/zitadel/internal/notification/templates"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type PasswordCodeData struct {
|
||||
templates.TemplateData
|
||||
FirstName string
|
||||
LastName string
|
||||
URL string
|
||||
}
|
||||
|
||||
func SendPasswordCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.PasswordCode, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getTwilioConfig func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string, origin string) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := login.InitPasswordLink(origin, user.ID, codeString, user.ResourceOwner)
|
||||
var args = mapNotifyUserToArgs(user)
|
||||
args["Code"] = codeString
|
||||
|
||||
passwordResetData := &PasswordCodeData{
|
||||
TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.PasswordResetMessageType, user.PreferredLanguage, colors),
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
URL: url,
|
||||
}
|
||||
template, err := templates.GetParsedTemplate(mailhtml, passwordResetData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if code.NotificationType == int32(domain.NotificationTypeSms) {
|
||||
return generateSms(ctx, user, passwordResetData.Text, getTwilioConfig, getFileSystemProvider, getLogProvider, false)
|
||||
}
|
||||
return generateEmail(ctx, user, passwordResetData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true)
|
||||
|
||||
func (notify Notify) SendPasswordCode(user *query.NotifyUser, origin, code string) error {
|
||||
url := login.InitPasswordLink(origin, user.ID, code, user.ResourceOwner)
|
||||
args := make(map[string]interface{})
|
||||
args["Code"] = code
|
||||
return notify(url, args, domain.PasswordResetMessageType, true)
|
||||
}
|
||||
|
@@ -1,42 +1,12 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/templates"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type PasswordlessRegistrationLinkData struct {
|
||||
templates.TemplateData
|
||||
URL string
|
||||
}
|
||||
|
||||
func SendPasswordlessRegistrationLink(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *user.HumanPasswordlessInitCodeRequestedEvent, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string, origin string) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := domain.PasswordlessInitCodeLink(origin+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, code.ID, codeString)
|
||||
var args = mapNotifyUserToArgs(user)
|
||||
|
||||
emailCodeData := &PasswordlessRegistrationLinkData{
|
||||
TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.PasswordlessRegistrationMessageType, user.PreferredLanguage, colors),
|
||||
URL: url,
|
||||
}
|
||||
|
||||
template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(ctx, user, emailCodeData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true)
|
||||
func (notify Notify) SendPasswordlessRegistrationLink(user *query.NotifyUser, origin, code, codeID string) error {
|
||||
url := domain.PasswordlessInitCodeLink(origin+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code)
|
||||
return notify(url, nil, domain.PasswordlessRegistrationMessageType, true)
|
||||
}
|
||||
|
@@ -1,38 +1,12 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
|
||||
"github.com/zitadel/zitadel/internal/notification/templates"
|
||||
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type PhoneVerificationCodeData struct {
|
||||
UserID string
|
||||
}
|
||||
|
||||
func SendPhoneVerificationCode(ctx context.Context, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.PhoneCode, getTwilioConfig func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var args = mapNotifyUserToArgs(user)
|
||||
args["Code"] = codeString
|
||||
|
||||
text := translator.Localize(fmt.Sprintf("%s.%s", domain.VerifyPhoneMessageType, domain.MessageText), args, user.PreferredLanguage)
|
||||
|
||||
codeData := &PhoneVerificationCodeData{UserID: user.ID}
|
||||
template, err := templates.ParseTemplateText(text, codeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateSms(ctx, user, template, getTwilioConfig, getFileSystemProvider, getLogProvider, true)
|
||||
func (notify Notify) SendPhoneVerificationCode(user *query.NotifyUser, origin, code string) error {
|
||||
args := make(map[string]interface{})
|
||||
args["Code"] = code
|
||||
return notify("", args, domain.VerifyPhoneMessageType, true)
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func GetTemplateData(translator *i18n.Translator, translateArgs map[string]interface{}, assetsPrefix, href, msgType, lang string, policy *query.LabelPolicy) templates.TemplateData {
|
||||
templateData := templates.TemplateData{
|
||||
Href: href,
|
||||
URL: href,
|
||||
PrimaryColor: templates.DefaultPrimaryColor,
|
||||
BackgroundColor: templates.DefaultBackgroundColor,
|
||||
FontColor: templates.DefaultFontColor,
|
||||
|
@@ -10,11 +10,10 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/messages"
|
||||
"github.com/zitadel/zitadel/internal/notification/senders"
|
||||
|
||||
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func generateEmail(ctx context.Context, user *view_model.NotifyUser, subject, content string, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastEmail bool) error {
|
||||
func generateEmail(ctx context.Context, user *query.NotifyUser, subject, content string, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastEmail bool) error {
|
||||
content = html.UnescapeString(content)
|
||||
message := &messages.Email{
|
||||
Recipients: []string{user.VerifiedEmail},
|
||||
@@ -36,20 +35,22 @@ func generateEmail(ctx context.Context, user *view_model.NotifyUser, subject, co
|
||||
return channelChain.HandleMessage(message)
|
||||
}
|
||||
|
||||
func mapNotifyUserToArgs(user *view_model.NotifyUser) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"UserName": user.UserName,
|
||||
"FirstName": user.FirstName,
|
||||
"LastName": user.LastName,
|
||||
"NickName": user.NickName,
|
||||
"DisplayName": user.DisplayName,
|
||||
"LastEmail": user.LastEmail,
|
||||
"VerifiedEmail": user.VerifiedEmail,
|
||||
"LastPhone": user.LastPhone,
|
||||
"VerifiedPhone": user.VerifiedPhone,
|
||||
"PreferredLoginName": user.PreferredLoginName,
|
||||
"LoginNames": user.LoginNames,
|
||||
"ChangeDate": user.ChangeDate,
|
||||
"CreationDate": user.CreationDate,
|
||||
func mapNotifyUserToArgs(user *query.NotifyUser, args map[string]interface{}) map[string]interface{} {
|
||||
if args == nil {
|
||||
args = make(map[string]interface{})
|
||||
}
|
||||
args["UserName"] = user.Username
|
||||
args["FirstName"] = user.FirstName
|
||||
args["LastName"] = user.LastName
|
||||
args["NickName"] = user.NickName
|
||||
args["DisplayName"] = user.DisplayName
|
||||
args["LastEmail"] = user.LastEmail
|
||||
args["VerifiedEmail"] = user.VerifiedEmail
|
||||
args["LastPhone"] = user.LastPhone
|
||||
args["VerifiedPhone"] = user.VerifiedPhone
|
||||
args["PreferredLoginName"] = user.PreferredLoginName
|
||||
args["LoginNames"] = user.LoginNames
|
||||
args["ChangeDate"] = user.ChangeDate
|
||||
args["CreationDate"] = user.CreationDate
|
||||
return args
|
||||
}
|
||||
|
@@ -3,20 +3,22 @@ package types
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
caos_errors "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/fs"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/log"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
|
||||
"github.com/zitadel/zitadel/internal/notification/messages"
|
||||
"github.com/zitadel/zitadel/internal/notification/senders"
|
||||
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func generateSms(ctx context.Context, user *view_model.NotifyUser, content string, getTwilioProvider func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastPhone bool) error {
|
||||
func generateSms(ctx context.Context, user *query.NotifyUser, content string, getTwilioProvider func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastPhone bool) error {
|
||||
number := ""
|
||||
twilio, err := getTwilioProvider(ctx)
|
||||
twilioConfig, err := getTwilioProvider(ctx)
|
||||
if err == nil {
|
||||
number = twilio.SenderNumber
|
||||
number = twilioConfig.SenderNumber
|
||||
}
|
||||
message := &messages.SMS{
|
||||
SenderPhoneNumber: number,
|
||||
@@ -27,7 +29,8 @@ func generateSms(ctx context.Context, user *view_model.NotifyUser, content strin
|
||||
message.RecipientPhoneNumber = user.LastPhone
|
||||
}
|
||||
|
||||
channelChain, err := senders.SMSChannels(ctx, twilio, getFileSystemProvider, getLogProvider)
|
||||
channelChain, err := senders.SMSChannels(ctx, twilioConfig, getFileSystemProvider, getLogProvider)
|
||||
logging.OnError(err).Error("could not create sms channel")
|
||||
|
||||
if channelChain.Len() == 0 {
|
||||
return caos_errors.ThrowPreconditionFailed(nil, "PHONE-w8nfow", "Errors.Notification.Channels.NotPresent")
|
||||
|
Reference in New Issue
Block a user