fix(notify): notify user in projection (#3889)

* start implement notify user in projection

* fix(stmt): add copy to multi stmt

* use projections for notify users

* feat: notifications from projections

* feat: notifications from projections

* cleanup

* pre-release

* fix tests

* fix types

* fix command

* fix queryNotifyUser

* fix: build version

* fix: HumanPasswordlessInitCodeSent

Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
This commit is contained in:
Livio Spring
2022-07-06 14:09:49 +02:00
committed by GitHub
parent d15a15c809
commit a1d404291d
46 changed files with 2018 additions and 1839 deletions

View File

@@ -1,38 +0,0 @@
package notification
import (
"database/sql"
"github.com/rakyll/statik/fs"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/notification/repository/eventsourcing"
_ "github.com/zitadel/zitadel/internal/notification/statik"
"github.com/zitadel/zitadel/internal/query"
)
type Config struct {
Repository eventsourcing.Config
}
func Start(config Config,
externalPort uint16,
externalSecure bool,
command *command.Commands,
queries *query.Queries,
dbClient *sql.DB,
assetsPrefix,
fileSystemPath string,
userEncryption crypto.EncryptionAlgorithm,
smtpEncryption crypto.EncryptionAlgorithm,
smsEncryption crypto.EncryptionAlgorithm,
) {
statikFS, err := fs.NewWithNamespace("notification")
logging.OnError(err).Panic("unable to start listener")
_, err = eventsourcing.Start(config.Repository, statikFS, externalPort, externalSecure, command, queries, dbClient, assetsPrefix, fileSystemPath, userEncryption, smtpEncryption, smsEncryption)
logging.OnError(err).Panic("unable to start app")
}

View File

@@ -0,0 +1,663 @@
package notification
import (
"context"
"net/http"
"time"
statik_fs "github.com/rakyll/statik/fs"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
_ "github.com/zitadel/zitadel/internal/notification/statik"
"github.com/zitadel/zitadel/internal/notification/types"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/repository/user"
)
const (
NotificationsProjectionTable = "projections.notifications"
NotifyUserID = "NOTIFICATION" //TODO: system?
)
func Start(ctx context.Context, customConfig projection.CustomConfig, externalPort uint16, externalSecure bool, commands *command.Commands, queries *query.Queries, es *eventstore.Eventstore, assetsPrefix func(context.Context) string, fileSystemPath string, userEncryption, smtpEncryption, smsEncryption crypto.EncryptionAlgorithm) {
statikFS, err := statik_fs.NewWithNamespace("notification")
logging.OnError(err).Panic("unable to start listener")
projection.NotificationsProjection = newNotificationsProjection(ctx, projection.ApplyCustomConfig(customConfig), commands, queries, es, userEncryption, smtpEncryption, smsEncryption, externalSecure, externalPort, fileSystemPath, assetsPrefix, statikFS)
}
type notificationsProjection struct {
crdb.StatementHandler
commands *command.Commands
queries *query.Queries
es *eventstore.Eventstore
userDataCrypto crypto.EncryptionAlgorithm
smtpPasswordCrypto crypto.EncryptionAlgorithm
smsTokenCrypto crypto.EncryptionAlgorithm
assetsPrefix func(context.Context) string
fileSystemPath string
externalPort uint16
externalSecure bool
statikDir http.FileSystem
}
func newNotificationsProjection(
ctx context.Context,
config crdb.StatementHandlerConfig,
commands *command.Commands,
queries *query.Queries,
es *eventstore.Eventstore,
userDataCrypto,
smtpPasswordCrypto,
smsTokenCrypto crypto.EncryptionAlgorithm,
externalSecure bool,
externalPort uint16,
fileSystemPath string,
assetsPrefix func(context.Context) string,
statikDir http.FileSystem,
) *notificationsProjection {
p := new(notificationsProjection)
config.ProjectionName = NotificationsProjectionTable
config.Reducers = p.reducers()
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
p.commands = commands
p.queries = queries
p.es = es
p.userDataCrypto = userDataCrypto
p.smtpPasswordCrypto = smtpPasswordCrypto
p.smsTokenCrypto = smsTokenCrypto
p.assetsPrefix = assetsPrefix
p.externalPort = externalPort
p.externalSecure = externalSecure
p.fileSystemPath = fileSystemPath
p.statikDir = statikDir
return p
}
func (p *notificationsProjection) reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: user.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: user.UserV1InitialCodeAddedType,
Reduce: p.reduceInitCodeAdded,
},
{
Event: user.HumanInitialCodeAddedType,
Reduce: p.reduceInitCodeAdded,
},
{
Event: user.UserV1EmailCodeAddedType,
Reduce: p.reduceEmailCodeAdded,
},
{
Event: user.HumanEmailCodeAddedType,
Reduce: p.reduceEmailCodeAdded,
},
{
Event: user.UserV1PasswordCodeAddedType,
Reduce: p.reducePasswordCodeAdded,
},
{
Event: user.HumanPasswordCodeAddedType,
Reduce: p.reducePasswordCodeAdded,
},
{
Event: user.UserDomainClaimedType,
Reduce: p.reduceDomainClaimed,
},
{
Event: user.HumanPasswordlessInitCodeRequestedType,
Reduce: p.reducePasswordlessCodeRequested,
},
{
Event: user.UserV1PhoneCodeAddedType,
Reduce: p.reducePhoneCodeAdded,
},
{
Event: user.HumanPhoneCodeAddedType,
Reduce: p.reducePhoneCodeAdded,
},
},
},
}
}
func (p *notificationsProjection) reduceInitCodeAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanInitialCodeAddedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-EFe2f", "reduce.wrong.event.type %s", user.HumanInitialCodeAddedType)
}
ctx := setNotificationContext(event.Aggregate())
alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
user.UserV1InitialCodeAddedType, user.UserV1InitialCodeSentType,
user.HumanInitialCodeAddedType, user.HumanInitialCodeSentType)
if err != nil {
return nil, err
}
if alreadyHandled {
return crdb.NewNoOpStatement(e), nil
}
code, err := crypto.DecryptString(e.Code, p.userDataCrypto)
if err != nil {
return nil, err
}
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
if err != nil {
return nil, err
}
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.InitCodeMessageType)
if err != nil {
return nil, err
}
origin, err := p.origin(ctx)
if err != nil {
return nil, err
}
err = types.SendEmail(
ctx,
string(template.Template),
translator,
notifyUser,
p.getSMTPConfig,
p.getFileSystemProvider,
p.getLogProvider,
colors,
p.assetsPrefix(ctx),
).SendUserInitCode(notifyUser, origin, code)
if err != nil {
return nil, err
}
err = p.commands.HumanInitCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
if err != nil {
return nil, err
}
return crdb.NewNoOpStatement(e), nil
}
func (p *notificationsProjection) reduceEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanEmailCodeAddedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType)
}
ctx := setNotificationContext(event.Aggregate())
alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType,
user.HumanEmailCodeAddedType, user.HumanEmailCodeSentType)
if err != nil {
return nil, err
}
if alreadyHandled {
return crdb.NewNoOpStatement(e), nil
}
code, err := crypto.DecryptString(e.Code, p.userDataCrypto)
if err != nil {
return nil, err
}
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
if err != nil {
return nil, err
}
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyEmailMessageType)
if err != nil {
return nil, err
}
origin, err := p.origin(ctx)
if err != nil {
return nil, err
}
err = types.SendEmail(
ctx,
string(template.Template),
translator,
notifyUser,
p.getSMTPConfig,
p.getFileSystemProvider,
p.getLogProvider,
colors,
p.assetsPrefix(ctx),
).SendEmailVerificationCode(notifyUser, origin, code)
if err != nil {
return nil, err
}
err = p.commands.HumanEmailVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
if err != nil {
return nil, err
}
return crdb.NewNoOpStatement(e), nil
}
func (p *notificationsProjection) reducePasswordCodeAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanPasswordCodeAddedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Eeg3s", "reduce.wrong.event.type %s", user.HumanPasswordCodeAddedType)
}
ctx := setNotificationContext(event.Aggregate())
alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
user.UserV1PasswordCodeAddedType, user.UserV1PasswordCodeSentType,
user.HumanPasswordCodeAddedType, user.HumanPasswordCodeSentType)
if err != nil {
return nil, err
}
if alreadyHandled {
return crdb.NewNoOpStatement(e), nil
}
code, err := crypto.DecryptString(e.Code, p.userDataCrypto)
if err != nil {
return nil, err
}
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
if err != nil {
return nil, err
}
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordResetMessageType)
if err != nil {
return nil, err
}
origin, err := p.origin(ctx)
if err != nil {
return nil, err
}
notify := types.SendEmail(
ctx,
string(template.Template),
translator,
notifyUser,
p.getSMTPConfig,
p.getFileSystemProvider,
p.getLogProvider,
colors,
p.assetsPrefix(ctx),
)
if e.NotificationType == domain.NotificationTypeSms {
notify = types.SendSMSTwilio(
ctx,
translator,
notifyUser,
p.getTwilioConfig,
p.getFileSystemProvider,
p.getLogProvider,
colors,
p.assetsPrefix(ctx),
)
}
err = notify.SendPasswordCode(notifyUser, origin, code)
if err != nil {
return nil, err
}
err = p.commands.PasswordCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
if err != nil {
return nil, err
}
return crdb.NewNoOpStatement(e), nil
}
func (p *notificationsProjection) reduceDomainClaimed(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.DomainClaimedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Drh5w", "reduce.wrong.event.type %s", user.UserDomainClaimedType)
}
ctx := setNotificationContext(event.Aggregate())
alreadyHandled, err := p.checkIfAlreadyHandled(ctx, event, nil,
user.UserDomainClaimedType, user.UserDomainClaimedSentType)
if err != nil {
return nil, err
}
if alreadyHandled {
return crdb.NewNoOpStatement(e), nil
}
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
if err != nil {
return nil, err
}
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.DomainClaimedMessageType)
if err != nil {
return nil, err
}
origin, err := p.origin(ctx)
if err != nil {
return nil, err
}
err = types.SendEmail(
ctx,
string(template.Template),
translator,
notifyUser,
p.getSMTPConfig,
p.getFileSystemProvider,
p.getLogProvider,
colors,
p.assetsPrefix(ctx),
).SendDomainClaimed(notifyUser, origin, e.UserName)
if err != nil {
return nil, err
}
err = p.commands.UserDomainClaimedSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
if err != nil {
return nil, err
}
return crdb.NewNoOpStatement(e), nil
}
func (p *notificationsProjection) reducePasswordlessCodeRequested(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanPasswordlessInitCodeRequestedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-EDtjd", "reduce.wrong.event.type %s", user.HumanPasswordlessInitCodeAddedType)
}
ctx := setNotificationContext(event.Aggregate())
alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, map[string]interface{}{"id": e.ID}, user.HumanPasswordlessInitCodeSentType)
if err != nil {
return nil, err
}
if alreadyHandled {
return crdb.NewNoOpStatement(e), nil
}
code, err := crypto.DecryptString(e.Code, p.userDataCrypto)
if err != nil {
return nil, err
}
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
if err != nil {
return nil, err
}
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordlessRegistrationMessageType)
if err != nil {
return nil, err
}
origin, err := p.origin(ctx)
if err != nil {
return nil, err
}
err = types.SendEmail(
ctx,
string(template.Template),
translator,
notifyUser,
p.getSMTPConfig,
p.getFileSystemProvider,
p.getLogProvider,
colors,
p.assetsPrefix(ctx),
).SendPasswordlessRegistrationLink(notifyUser, origin, code, e.ID)
if err != nil {
return nil, err
}
err = p.commands.HumanPasswordlessInitCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner, e.ID)
if err != nil {
return nil, err
}
return crdb.NewNoOpStatement(e), nil
}
func (p *notificationsProjection) reducePhoneCodeAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanPhoneCodeAddedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-He83g", "reduce.wrong.event.type %s", user.HumanPhoneCodeAddedType)
}
ctx := setNotificationContext(event.Aggregate())
alreadyHandled, err := p.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
user.UserV1PhoneCodeAddedType, user.UserV1PhoneCodeSentType,
user.HumanPhoneCodeAddedType, user.HumanPhoneCodeSentType)
if err != nil {
return nil, err
}
if alreadyHandled {
return crdb.NewNoOpStatement(e), nil
}
code, err := crypto.DecryptString(e.Code, p.userDataCrypto)
if err != nil {
return nil, err
}
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID)
if err != nil {
return nil, err
}
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyPhoneMessageType)
if err != nil {
return nil, err
}
origin, err := p.origin(ctx)
if err != nil {
return nil, err
}
err = types.SendSMSTwilio(
ctx,
translator,
notifyUser,
p.getTwilioConfig,
p.getFileSystemProvider,
p.getLogProvider,
colors,
p.assetsPrefix(ctx),
).SendPhoneVerificationCode(notifyUser, origin, code)
if err != nil {
return nil, err
}
err = p.commands.HumanPhoneVerificationCodeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
if err != nil {
return nil, err
}
return crdb.NewNoOpStatement(e), nil
}
func (p *notificationsProjection) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event eventstore.Event, expiry time.Duration, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) {
if event.CreationDate().Add(expiry).Before(time.Now().UTC()) {
return true, nil
}
return p.checkIfAlreadyHandled(ctx, event, data, eventTypes...)
}
func (p *notificationsProjection) checkIfAlreadyHandled(ctx context.Context, event eventstore.Event, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) {
events, err := p.es.Filter(
ctx,
eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
InstanceID(event.Aggregate().InstanceID).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(event.Aggregate().ID).
SequenceGreater(event.Sequence()).
EventTypes(eventTypes...).
EventData(data).
Builder(),
)
if err != nil {
return false, err
}
return len(events) > 0, nil
}
func (p *notificationsProjection) getSMTPConfig(ctx context.Context) (*smtp.EmailConfig, error) {
config, err := p.queries.SMTPConfigByAggregateID(ctx, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
password, err := crypto.DecryptString(config.Password, p.smtpPasswordCrypto)
if err != nil {
return nil, err
}
return &smtp.EmailConfig{
From: config.SenderAddress,
FromName: config.SenderName,
Tls: config.TLS,
SMTP: smtp.SMTP{
Host: config.Host,
User: config.User,
Password: password,
},
}, nil
}
// Read iam twilio config
func (p *notificationsProjection) getTwilioConfig(ctx context.Context) (*twilio.TwilioConfig, error) {
active, err := query.NewSMSProviderStateQuery(domain.SMSConfigStateActive)
if err != nil {
return nil, err
}
config, err := p.queries.SMSProviderConfig(ctx, active)
if err != nil {
return nil, err
}
if config.TwilioConfig == nil {
return nil, errors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound")
}
token, err := crypto.DecryptString(config.TwilioConfig.Token, p.smsTokenCrypto)
if err != nil {
return nil, err
}
return &twilio.TwilioConfig{
SID: config.TwilioConfig.SID,
Token: token,
SenderNumber: config.TwilioConfig.SenderNumber,
}, nil
}
// Read iam filesystem provider config
func (p *notificationsProjection) getFileSystemProvider(ctx context.Context) (*fs.FSConfig, error) {
config, err := p.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeFile)
if err != nil {
return nil, err
}
return &fs.FSConfig{
Compact: config.Compact,
Path: p.fileSystemPath,
}, nil
}
// Read iam log provider config
func (p *notificationsProjection) getLogProvider(ctx context.Context) (*log.LogConfig, error) {
config, err := p.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeLog)
if err != nil {
return nil, err
}
return &log.LogConfig{
Compact: config.Compact,
}, nil
}
func (p *notificationsProjection) getTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) {
translator, err := i18n.NewTranslator(p.statikDir, p.queries.GetDefaultLanguage(ctx), "")
if err != nil {
return nil, err
}
allCustomTexts, err := p.queries.CustomTextListByTemplate(ctx, authz.GetInstance(ctx).InstanceID(), textType)
if err != nil {
return translator, nil
}
customTexts, err := p.queries.CustomTextListByTemplate(ctx, orgID, textType)
if err != nil {
return translator, nil
}
allCustomTexts.CustomTexts = append(allCustomTexts.CustomTexts, customTexts.CustomTexts...)
for _, text := range allCustomTexts.CustomTexts {
msg := i18n.Message{
ID: text.Template + "." + text.Key,
Text: text.Text,
}
err = translator.AddMessages(text.Language, msg)
logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "orgID", orgID, "messageType", textType, "messageID", msg.ID).
OnError(err).
Warn("could not add translation message")
}
return translator, nil
}
func (p *notificationsProjection) origin(ctx context.Context) (string, error) {
primary, err := query.NewInstanceDomainPrimarySearchQuery(true)
if err != nil {
return "", err
}
domains, err := p.queries.SearchInstanceDomains(ctx, &query.InstanceDomainSearchQueries{
Queries: []query.SearchQuery{primary},
})
if err != nil {
return "", err
}
if len(domains.Domains) < 1 {
return "", errors.ThrowInternal(nil, "NOTIF-Ef3r1", "Errors.Notification.NoDomain")
}
return http_utils.BuildHTTP(domains.Domains[0].Domain, p.externalPort, p.externalSecure), nil
}
func setNotificationContext(event eventstore.Aggregate) context.Context {
ctx := authz.WithInstanceID(context.Background(), event.InstanceID)
return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: event.ResourceOwner})
}

View File

@@ -1,89 +0,0 @@
package handler
import (
"net/http"
"time"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
queryv1 "github.com/zitadel/zitadel/internal/eventstore/v1/query"
"github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/query"
)
type Configs map[string]*Config
type Config struct {
MinimumCycleDuration time.Duration
}
type handler struct {
view *view.View
bulkLimit uint64
cycleDuration time.Duration
errorCountUntilSkip uint64
es v1.Eventstore
}
func (h *handler) Eventstore() v1.Eventstore {
return h.es
}
func Register(configs Configs,
bulkLimit,
errorCount uint64,
view *view.View,
es v1.Eventstore,
command *command.Commands,
queries *query.Queries,
externalPort uint16,
externalSecure bool,
dir http.FileSystem,
assetsPrefix,
fileSystemPath string,
userEncryption crypto.EncryptionAlgorithm,
smtpEncryption crypto.EncryptionAlgorithm,
smsEncryption crypto.EncryptionAlgorithm,
) []queryv1.Handler {
return []queryv1.Handler{
newNotifyUser(
handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es},
queries,
),
newNotification(
handler{view, bulkLimit, configs.cycleDuration("Notification"), errorCount, es},
command,
queries,
externalPort,
externalSecure,
dir,
assetsPrefix,
fileSystemPath,
userEncryption,
smtpEncryption,
smsEncryption,
),
}
}
func (configs Configs) cycleDuration(viewModel string) time.Duration {
c, ok := configs[viewModel]
if !ok {
return 1 * time.Minute
}
return c.MinimumCycleDuration
}
func (h *handler) MinimumCycleDuration() time.Duration {
return h.cycleDuration
}
func (h *handler) LockDuration() time.Duration {
return h.cycleDuration / 3
}
func (h *handler) QueryLimit() uint64 {
return h.bulkLimit
}

View File

@@ -1,637 +0,0 @@
package handler
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
queryv1 "github.com/zitadel/zitadel/internal/eventstore/v1/query"
"github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
"github.com/zitadel/zitadel/internal/notification/types"
"github.com/zitadel/zitadel/internal/query"
user_repo "github.com/zitadel/zitadel/internal/repository/user"
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
"github.com/zitadel/zitadel/internal/user/repository/view"
"github.com/zitadel/zitadel/internal/user/repository/view/model"
)
const (
notificationTable = "notification.notifications"
NotifyUserID = "NOTIFICATION"
)
type Notification struct {
handler
command *command.Commands
fileSystemPath string
statikDir http.FileSystem
subscription *v1.Subscription
assetsPrefix string
queries *query.Queries
userDataCrypto crypto.EncryptionAlgorithm
smtpPasswordCrypto crypto.EncryptionAlgorithm
smsTokenCrypto crypto.EncryptionAlgorithm
externalPort uint16
externalSecure bool
}
func newNotification(
handler handler,
command *command.Commands,
query *query.Queries,
externalPort uint16,
externalSecure bool,
statikDir http.FileSystem,
assetsPrefix,
fileSystemPath string,
userEncryption crypto.EncryptionAlgorithm,
smtpEncryption crypto.EncryptionAlgorithm,
smsEncryption crypto.EncryptionAlgorithm,
) *Notification {
h := &Notification{
handler: handler,
command: command,
statikDir: statikDir,
assetsPrefix: assetsPrefix,
queries: query,
userDataCrypto: userEncryption,
smtpPasswordCrypto: smtpEncryption,
smsTokenCrypto: smsEncryption,
externalSecure: externalSecure,
externalPort: externalPort,
fileSystemPath: fileSystemPath,
}
h.subscribe()
return h
}
func (k *Notification) subscribe() {
k.subscription = k.es.Subscribe(k.AggregateTypes()...)
go func() {
for event := range k.subscription.Events {
queryv1.ReduceEvent(k, event)
}
}()
}
func (n *Notification) ViewModel() string {
return notificationTable
}
func (n *Notification) Subscription() *v1.Subscription {
return n.subscription
}
func (_ *Notification) AggregateTypes() []models.AggregateType {
return []models.AggregateType{user_repo.AggregateType}
}
func (n *Notification) CurrentSequence(instanceID string) (uint64, error) {
sequence, err := n.view.GetLatestNotificationSequence(instanceID)
if err != nil {
return 0, err
}
return sequence.CurrentSequence, nil
}
func (n *Notification) EventQuery() (*models.SearchQuery, error) {
sequences, err := n.view.GetLatestNotificationSequences()
if err != nil {
return nil, err
}
query := models.NewSearchQuery()
instances := make([]string, 0)
for _, sequence := range sequences {
for _, instance := range instances {
if sequence.InstanceID == instance {
break
}
}
instances = append(instances, sequence.InstanceID)
query.AddQuery().
AggregateTypeFilter(n.AggregateTypes()...).
LatestSequenceFilter(sequence.CurrentSequence).
InstanceIDFilter(sequence.InstanceID)
}
return query.AddQuery().
AggregateTypeFilter(n.AggregateTypes()...).
LatestSequenceFilter(0).
ExcludedInstanceIDsFilter(instances...).
SearchQuery(), nil
}
func (n *Notification) Reduce(event *models.Event) (err error) {
switch eventstore.EventType(event.Type) {
case user_repo.UserV1InitialCodeAddedType,
user_repo.HumanInitialCodeAddedType:
err = n.handleInitUserCode(event)
case user_repo.UserV1EmailCodeAddedType,
user_repo.HumanEmailCodeAddedType:
err = n.handleEmailVerificationCode(event)
case user_repo.UserV1PhoneCodeAddedType,
user_repo.HumanPhoneCodeAddedType:
err = n.handlePhoneVerificationCode(event)
case user_repo.UserV1PasswordCodeAddedType,
user_repo.HumanPasswordCodeAddedType:
err = n.handlePasswordCode(event)
case user_repo.UserDomainClaimedType:
err = n.handleDomainClaimed(event)
case user_repo.HumanPasswordlessInitCodeRequestedType:
err = n.handlePasswordlessRegistrationLink(event)
}
if err != nil {
return err
}
return n.view.ProcessedNotificationSequence(event)
}
func (n *Notification) handleInitUserCode(event *models.Event) (err error) {
initCode := new(es_model.InitUserCode)
if err := initCode.SetData(event); err != nil {
return err
}
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, initCode.Expiry,
user_repo.UserV1InitialCodeAddedType, user_repo.UserV1InitialCodeSentType,
user_repo.HumanInitialCodeAddedType, user_repo.HumanInitialCodeSentType)
if err != nil || alreadyHandled {
return err
}
colors, err := n.getLabelPolicy(ctx)
if err != nil {
return err
}
template, err := n.getMailTemplate(ctx)
if err != nil {
return err
}
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
if err != nil && !errors.IsNotFound(err) {
return err
}
if user.Sequence < event.Sequence {
if err = n.verifyLatestUser(ctx, user); err != nil {
return err
}
}
if user.Sequence == 0 {
return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found")
}
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.InitCodeMessageType)
if err != nil {
return err
}
origin, err := n.origin(ctx)
if err != nil {
return err
}
err = types.SendUserInitCode(ctx, string(template.Template), translator, user, initCode, n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin)
if err != nil {
return err
}
return n.command.HumanInitCodeSent(ctx, event.ResourceOwner, event.AggregateID)
}
func (n *Notification) handlePasswordCode(event *models.Event) (err error) {
pwCode := new(es_model.PasswordCode)
if err := pwCode.SetData(event); err != nil {
return err
}
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, pwCode.Expiry,
user_repo.UserV1PasswordCodeAddedType, user_repo.UserV1PasswordCodeSentType,
user_repo.HumanPasswordCodeAddedType, user_repo.HumanPasswordCodeSentType)
if err != nil || alreadyHandled {
return err
}
colors, err := n.getLabelPolicy(ctx)
if err != nil {
return err
}
template, err := n.getMailTemplate(ctx)
if err != nil {
return err
}
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
if err != nil && !errors.IsNotFound(err) {
return err
}
if user.Sequence < event.Sequence {
if err = n.verifyLatestUser(ctx, user); err != nil {
return err
}
}
if user.Sequence == 0 {
return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found")
}
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.PasswordResetMessageType)
if err != nil {
return err
}
origin, err := n.origin(ctx)
if err != nil {
return err
}
err = types.SendPasswordCode(ctx, string(template.Template), translator, user, pwCode, n.getSMTPConfig, n.getTwilioConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin)
if err != nil {
return err
}
return n.command.PasswordCodeSent(ctx, event.ResourceOwner, event.AggregateID)
}
func (n *Notification) handleEmailVerificationCode(event *models.Event) (err error) {
emailCode := new(es_model.EmailCode)
if err := emailCode.SetData(event); err != nil {
return err
}
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, emailCode.Expiry,
user_repo.UserV1EmailCodeAddedType, user_repo.UserV1EmailCodeSentType,
user_repo.HumanEmailCodeAddedType, user_repo.HumanEmailCodeSentType)
if err != nil || alreadyHandled {
return nil
}
colors, err := n.getLabelPolicy(ctx)
if err != nil {
return err
}
template, err := n.getMailTemplate(ctx)
if err != nil {
return err
}
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
if err != nil && !errors.IsNotFound(err) {
return err
}
if user.Sequence < event.Sequence {
if err = n.verifyLatestUser(ctx, user); err != nil {
return err
}
}
if user.Sequence == 0 {
return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found")
}
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.VerifyEmailMessageType)
if err != nil {
return err
}
origin, err := n.origin(ctx)
if err != nil {
return err
}
err = types.SendEmailVerificationCode(ctx, string(template.Template), translator, user, emailCode, n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin)
if err != nil {
return err
}
return n.command.HumanEmailVerificationCodeSent(ctx, event.ResourceOwner, event.AggregateID)
}
func (n *Notification) handlePhoneVerificationCode(event *models.Event) (err error) {
phoneCode := new(es_model.PhoneCode)
if err := phoneCode.SetData(event); err != nil {
return err
}
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(ctx, event, phoneCode.Expiry,
user_repo.UserV1PhoneCodeAddedType, user_repo.UserV1PhoneCodeSentType,
user_repo.HumanPhoneCodeAddedType, user_repo.HumanPhoneCodeSentType)
if err != nil || alreadyHandled {
return nil
}
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
if err != nil && !errors.IsNotFound(err) {
return err
}
if user.Sequence < event.Sequence {
if err = n.verifyLatestUser(ctx, user); err != nil {
return err
}
}
if user.Sequence == 0 {
return errors.ThrowNotFound(nil, "HANDL-JED2R", "no user events found")
}
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.VerifyPhoneMessageType)
if err != nil {
return err
}
err = types.SendPhoneVerificationCode(ctx, translator, user, phoneCode, n.getTwilioConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto)
if err != nil {
return err
}
return n.command.HumanPhoneVerificationCodeSent(ctx, event.ResourceOwner, event.AggregateID)
}
func (n *Notification) handleDomainClaimed(event *models.Event) (err error) {
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
alreadyHandled, err := n.checkIfAlreadyHandled(ctx, event.AggregateID, event.InstanceID, event.Sequence, user_repo.UserDomainClaimedType, user_repo.UserDomainClaimedSentType)
if err != nil || alreadyHandled {
return nil
}
data := make(map[string]string)
if err := json.Unmarshal(event.Data, &data); err != nil {
logging.Log("HANDLE-Gghq2").WithError(err).Error("could not unmarshal event data")
return errors.ThrowInternal(err, "HANDLE-7hgj3", "could not unmarshal event")
}
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
if err != nil {
return err
}
if user.LastEmail == "" {
return nil
}
colors, err := n.getLabelPolicy(ctx)
if err != nil {
return err
}
template, err := n.getMailTemplate(ctx)
if err != nil {
return err
}
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.DomainClaimedMessageType)
if err != nil {
return err
}
origin, err := n.origin(ctx)
if err != nil {
return err
}
err = types.SendDomainClaimed(ctx, string(template.Template), translator, user, data["userName"], n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, colors, n.assetsPrefix, origin)
if err != nil {
return err
}
return n.command.UserDomainClaimedSent(ctx, event.ResourceOwner, event.AggregateID)
}
func (n *Notification) handlePasswordlessRegistrationLink(event *models.Event) (err error) {
addedEvent := new(user_repo.HumanPasswordlessInitCodeRequestedEvent)
if err := json.Unmarshal(event.Data, addedEvent); err != nil {
return err
}
ctx := getSetNotifyContextData(event.InstanceID, event.ResourceOwner)
events, err := n.getUserEvents(ctx, event.AggregateID, event.InstanceID, event.Sequence)
if err != nil {
return err
}
for _, e := range events {
if eventstore.EventType(e.Type) == user_repo.HumanPasswordlessInitCodeSentType {
sentEvent := new(user_repo.HumanPasswordlessInitCodeSentEvent)
if err := json.Unmarshal(e.Data, sentEvent); err != nil {
return err
}
if sentEvent.ID == addedEvent.ID {
return nil
}
}
}
user, err := n.getUserByID(event.AggregateID, event.InstanceID)
if err != nil {
return err
}
colors, err := n.getLabelPolicy(ctx)
if err != nil {
return err
}
template, err := n.getMailTemplate(ctx)
if err != nil {
return err
}
translator, err := n.getTranslatorWithOrgTexts(ctx, user.ResourceOwner, domain.PasswordlessRegistrationMessageType)
if err != nil {
return err
}
origin, err := n.origin(ctx)
if err != nil {
return err
}
err = types.SendPasswordlessRegistrationLink(ctx, string(template.Template), translator, user, addedEvent, n.getSMTPConfig, n.getFileSystemProvider, n.getLogProvider, n.userDataCrypto, colors, n.assetsPrefix, origin)
if err != nil {
return err
}
return n.command.HumanPasswordlessInitCodeSent(ctx, event.AggregateID, event.ResourceOwner, addedEvent.ID)
}
func (n *Notification) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event *models.Event, expiry time.Duration, eventTypes ...eventstore.EventType) (bool, error) {
if event.CreationDate.Add(expiry).Before(time.Now().UTC()) {
return true, nil
}
return n.checkIfAlreadyHandled(ctx, event.AggregateID, event.InstanceID, event.Sequence, eventTypes...)
}
func (n *Notification) checkIfAlreadyHandled(ctx context.Context, userID, instanceID string, sequence uint64, eventTypes ...eventstore.EventType) (bool, error) {
events, err := n.getUserEvents(ctx, userID, instanceID, sequence)
if err != nil {
return false, err
}
for _, event := range events {
for _, eventType := range eventTypes {
if eventstore.EventType(event.Type) == eventType {
return true, nil
}
}
}
return false, nil
}
func (n *Notification) getUserEvents(ctx context.Context, userID, instanceID string, sequence uint64) ([]*models.Event, error) {
query, err := view.UserByIDQuery(userID, instanceID, sequence)
if err != nil {
return nil, err
}
return n.es.FilterEvents(ctx, query)
}
func (n *Notification) OnError(event *models.Event, err error) error {
logging.WithFields("id", event.AggregateID, "sequence", event.Sequence).WithError(err).Warn("something went wrong in notification handler")
return spooler.HandleError(event, err, n.view.GetLatestNotificationFailedEvent, n.view.ProcessedNotificationFailedEvent, n.view.ProcessedNotificationSequence, n.errorCountUntilSkip)
}
func (n *Notification) OnSuccess() error {
return spooler.HandleSuccess(n.view.UpdateNotificationSpoolerRunTimestamp)
}
func getSetNotifyContextData(instanceID, orgID string) context.Context {
ctx := authz.WithInstanceID(context.Background(), instanceID)
return authz.SetCtxData(ctx, authz.CtxData{UserID: NotifyUserID, OrgID: orgID})
}
// Read organization specific colors
func (n *Notification) getLabelPolicy(ctx context.Context) (*query.LabelPolicy, error) {
return n.queries.ActiveLabelPolicyByOrg(ctx, authz.GetCtxData(ctx).OrgID)
}
// Read organization specific template
func (n *Notification) getMailTemplate(ctx context.Context) (*query.MailTemplate, error) {
return n.queries.MailTemplateByOrg(ctx, authz.GetCtxData(ctx).OrgID)
}
// Read iam smtp config
func (n *Notification) getSMTPConfig(ctx context.Context) (*smtp.EmailConfig, error) {
config, err := n.queries.SMTPConfigByAggregateID(ctx, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
password, err := crypto.Decrypt(config.Password, n.smtpPasswordCrypto)
if err != nil {
return nil, err
}
return &smtp.EmailConfig{
From: config.SenderAddress,
FromName: config.SenderName,
Tls: config.TLS,
SMTP: smtp.SMTP{
Host: config.Host,
User: config.User,
Password: string(password),
},
}, nil
}
// Read iam twilio config
func (n *Notification) getTwilioConfig(ctx context.Context) (*twilio.TwilioConfig, error) {
active, err := query.NewSMSProviderStateQuery(domain.SMSConfigStateActive)
if err != nil {
return nil, err
}
config, err := n.queries.SMSProviderConfig(ctx, active)
if err != nil {
return nil, err
}
if config.TwilioConfig == nil {
return nil, errors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound")
}
token, err := crypto.Decrypt(config.TwilioConfig.Token, n.smsTokenCrypto)
if err != nil {
return nil, err
}
return &twilio.TwilioConfig{
SID: config.TwilioConfig.SID,
Token: string(token),
SenderNumber: config.TwilioConfig.SenderNumber,
}, nil
}
// Read iam filesystem provider config
func (n *Notification) getFileSystemProvider(ctx context.Context) (*fs.FSConfig, error) {
config, err := n.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeFile)
if err != nil {
return nil, err
}
return &fs.FSConfig{
Compact: config.Compact,
Path: n.fileSystemPath,
}, nil
}
// Read iam log provider config
func (n *Notification) getLogProvider(ctx context.Context) (*log.LogConfig, error) {
config, err := n.queries.NotificationProviderByIDAndType(ctx, authz.GetInstance(ctx).InstanceID(), domain.NotificationProviderTypeLog)
if err != nil {
return nil, err
}
return &log.LogConfig{
Compact: config.Compact,
}, nil
}
func (n *Notification) getTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) {
translator, err := i18n.NewTranslator(n.statikDir, n.queries.GetDefaultLanguage(ctx), "")
if err != nil {
return nil, err
}
allCustomTexts, err := n.queries.CustomTextListByTemplate(ctx, authz.GetInstance(ctx).InstanceID(), textType)
if err != nil {
return translator, nil
}
customTexts, err := n.queries.CustomTextListByTemplate(ctx, orgID, textType)
if err != nil {
return translator, nil
}
allCustomTexts.CustomTexts = append(allCustomTexts.CustomTexts, customTexts.CustomTexts...)
for _, text := range allCustomTexts.CustomTexts {
msg := i18n.Message{
ID: text.Template + "." + text.Key,
Text: text.Text,
}
translator.AddMessages(text.Language, msg)
}
return translator, nil
}
func (n *Notification) getUserByID(userID, instanceID string) (*model.NotifyUser, error) {
return n.view.NotifyUserByID(userID, instanceID)
}
func (n *Notification) origin(ctx context.Context) (string, error) {
primary, err := query.NewInstanceDomainPrimarySearchQuery(true)
domains, err := n.queries.SearchInstanceDomains(ctx, &query.InstanceDomainSearchQueries{
Queries: []query.SearchQuery{primary},
})
if err != nil {
return "", err
}
if len(domains.Domains) < 1 {
return "", errors.ThrowInternal(nil, "NOTIF-Ef3r1", "Errors.Notification.NoDomain")
}
return http_utils.BuildHTTP(domains.Domains[0].Domain, n.externalPort, n.externalSecure), nil
}
func (n *Notification) verifyLatestUser(ctx context.Context, user *model.NotifyUser) error {
events, err := n.getUserEvents(ctx, user.ID, user.InstanceID, user.Sequence)
if err != nil {
return err
}
for _, event := range events {
if err = user.AppendEvent(event); err != nil {
return err
}
}
return nil
}

View File

@@ -1,278 +0,0 @@
package handler
import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/eventstore/v1/query"
es_sdk "github.com/zitadel/zitadel/internal/eventstore/v1/sdk"
"github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
org_model "github.com/zitadel/zitadel/internal/org/model"
org_es_model "github.com/zitadel/zitadel/internal/org/repository/eventsourcing/model"
org_view "github.com/zitadel/zitadel/internal/org/repository/view"
query2 "github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/user"
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
)
const (
userTable = "notification.notify_users"
)
type NotifyUser struct {
handler
subscription *v1.Subscription
queries *query2.Queries
}
func newNotifyUser(
handler handler,
queries *query2.Queries,
) *NotifyUser {
h := &NotifyUser{
handler: handler,
queries: queries,
}
h.subscribe()
return h
}
func (k *NotifyUser) subscribe() {
k.subscription = k.es.Subscribe(k.AggregateTypes()...)
go func() {
for event := range k.subscription.Events {
query.ReduceEvent(k, event)
}
}()
}
func (p *NotifyUser) ViewModel() string {
return userTable
}
func (p *NotifyUser) Subscription() *v1.Subscription {
return p.subscription
}
func (_ *NotifyUser) AggregateTypes() []es_models.AggregateType {
return []es_models.AggregateType{user.AggregateType, org.AggregateType}
}
func (p *NotifyUser) CurrentSequence(instanceID string) (uint64, error) {
sequence, err := p.view.GetLatestNotifyUserSequence(instanceID)
if err != nil {
return 0, err
}
return sequence.CurrentSequence, nil
}
func (p *NotifyUser) EventQuery() (*es_models.SearchQuery, error) {
sequences, err := p.view.GetLatestNotifyUserSequences()
if err != nil {
return nil, err
}
query := es_models.NewSearchQuery()
instances := make([]string, 0)
for _, sequence := range sequences {
for _, instance := range instances {
if sequence.InstanceID == instance {
break
}
}
instances = append(instances, sequence.InstanceID)
query.AddQuery().
AggregateTypeFilter(p.AggregateTypes()...).
LatestSequenceFilter(sequence.CurrentSequence).
InstanceIDFilter(sequence.InstanceID)
}
return query.AddQuery().
AggregateTypeFilter(p.AggregateTypes()...).
LatestSequenceFilter(0).
ExcludedInstanceIDsFilter(instances...).
SearchQuery(), nil
}
func (u *NotifyUser) Reduce(event *es_models.Event) (err error) {
switch event.AggregateType {
case user.AggregateType:
return u.ProcessUser(event)
case org.AggregateType:
return u.ProcessOrg(event)
default:
return nil
}
}
func (u *NotifyUser) ProcessUser(event *es_models.Event) (err error) {
notifyUser := new(view_model.NotifyUser)
switch eventstore.EventType(event.Type) {
case user.UserV1AddedType,
user.UserV1RegisteredType,
user.HumanRegisteredType,
user.HumanAddedType,
user.MachineAddedEventType:
err = notifyUser.AppendEvent(event)
if err != nil {
return err
}
err = u.fillLoginNames(notifyUser)
case user.UserV1ProfileChangedType,
user.UserV1EmailChangedType,
user.UserV1EmailVerifiedType,
user.UserV1PhoneChangedType,
user.UserV1PhoneVerifiedType,
user.UserV1PhoneRemovedType,
user.HumanProfileChangedType,
user.HumanEmailChangedType,
user.HumanEmailVerifiedType,
user.HumanPhoneChangedType,
user.HumanPhoneVerifiedType,
user.HumanPhoneRemovedType,
user.MachineChangedEventType:
notifyUser, err = u.view.NotifyUserByID(event.AggregateID, event.InstanceID)
if err != nil {
return err
}
err = notifyUser.AppendEvent(event)
case user.UserDomainClaimedType,
user.UserUserNameChangedType:
notifyUser, err = u.view.NotifyUserByID(event.AggregateID, event.InstanceID)
if err != nil {
return err
}
err = notifyUser.AppendEvent(event)
if err != nil {
return err
}
err = u.fillLoginNames(notifyUser)
case user.UserRemovedType:
return u.view.DeleteNotifyUser(event.AggregateID, event.InstanceID, event)
default:
return u.view.ProcessedNotifyUserSequence(event)
}
if err != nil {
return err
}
return u.view.PutNotifyUser(notifyUser, event)
}
func (u *NotifyUser) ProcessOrg(event *es_models.Event) (err error) {
switch eventstore.EventType(event.Type) {
case org.OrgDomainVerifiedEventType,
org.OrgDomainRemovedEventType,
org.DomainPolicyAddedEventType,
org.DomainPolicyChangedEventType,
org.DomainPolicyRemovedEventType:
return u.fillLoginNamesOnOrgUsers(event)
case org.OrgDomainPrimarySetEventType:
return u.fillPreferredLoginNamesOnOrgUsers(event)
default:
return u.view.ProcessedNotifyUserSequence(event)
}
}
func (u *NotifyUser) fillLoginNamesOnOrgUsers(event *es_models.Event) error {
userLoginMustBeDomain, _, domains, err := u.loginNameInformation(context.Background(), event.ResourceOwner, event.InstanceID)
if err != nil {
return err
}
users, err := u.view.NotifyUsersByOrgID(event.AggregateID, event.InstanceID)
if err != nil {
return err
}
for _, user := range users {
user.SetLoginNames(userLoginMustBeDomain, domains)
err := u.view.PutNotifyUser(user, event)
if err != nil {
return err
}
}
return u.view.ProcessedNotifyUserSequence(event)
}
func (u *NotifyUser) fillPreferredLoginNamesOnOrgUsers(event *es_models.Event) error {
userLoginMustBeDomain, primaryDomain, _, err := u.loginNameInformation(context.Background(), event.ResourceOwner, event.InstanceID)
if err != nil {
return err
}
if !userLoginMustBeDomain {
return nil
}
users, err := u.view.NotifyUsersByOrgID(event.AggregateID, event.InstanceID)
if err != nil {
return err
}
for _, user := range users {
user.PreferredLoginName = user.GenerateLoginName(primaryDomain, userLoginMustBeDomain)
err := u.view.PutNotifyUser(user, event)
if err != nil {
return err
}
}
return nil
}
func (u *NotifyUser) fillLoginNames(user *view_model.NotifyUser) (err error) {
userLoginMustBeDomain, primaryDomain, domains, err := u.loginNameInformation(context.Background(), user.ResourceOwner, user.InstanceID)
if err != nil {
return err
}
user.SetLoginNames(userLoginMustBeDomain, domains)
user.PreferredLoginName = user.GenerateLoginName(primaryDomain, userLoginMustBeDomain)
return nil
}
func (p *NotifyUser) OnError(event *es_models.Event, err error) error {
logging.LogWithFields("SPOOL-9spwf", "id", event.AggregateID).WithError(err).Warn("something went wrong in notify user handler")
return spooler.HandleError(event, err, p.view.GetLatestNotifyUserFailedEvent, p.view.ProcessedNotifyUserFailedEvent, p.view.ProcessedNotifyUserSequence, p.errorCountUntilSkip)
}
func (u *NotifyUser) OnSuccess() error {
return spooler.HandleSuccess(u.view.UpdateNotifyUserSpoolerRunTimestamp)
}
func (u *NotifyUser) getOrgByID(ctx context.Context, orgID, instanceID string) (*org_model.Org, error) {
query, err := org_view.OrgByIDQuery(orgID, instanceID, 0)
if err != nil {
return nil, err
}
esOrg := &org_es_model.Org{
ObjectRoot: es_models.ObjectRoot{
AggregateID: orgID,
},
}
err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, query)
if err != nil && !caos_errs.IsNotFound(err) {
return nil, err
}
if esOrg.Sequence == 0 {
return nil, caos_errs.ThrowNotFound(nil, "EVENT-kVLb2", "Errors.Org.NotFound")
}
return org_es_model.OrgToModel(esOrg), nil
}
func (u *NotifyUser) loginNameInformation(ctx context.Context, orgID, instanceID string) (userLoginMustBeDomain bool, primaryDomain string, domains []*org_model.OrgDomain, err error) {
org, err := u.getOrgByID(ctx, orgID, instanceID)
if err != nil {
return false, "", nil, err
}
if org.DomainPolicy == nil {
policy, err := u.queries.DefaultDomainPolicy(authz.WithInstanceID(ctx, org.InstanceID))
if err != nil {
return false, "", nil, err
}
userLoginMustBeDomain = policy.UserLoginMustBeDomain
}
return userLoginMustBeDomain, org.GetPrimaryDomain().Domain, org.Domains, nil
}

View File

@@ -1,56 +0,0 @@
package eventsourcing
import (
"database/sql"
"net/http"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
es_spol "github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
"github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/spooler"
noti_view "github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/query"
)
type Config struct {
Spooler spooler.SpoolerConfig
}
type EsRepository struct {
spooler *es_spol.Spooler
}
func Start(conf Config,
dir http.FileSystem,
externalPort uint16,
externalSecure bool,
command *command.Commands,
queries *query.Queries,
dbClient *sql.DB,
assetsPrefix,
fileSystemPath string,
userEncryption crypto.EncryptionAlgorithm,
smtpEncryption crypto.EncryptionAlgorithm,
smsEncryption crypto.EncryptionAlgorithm,
) (*EsRepository, error) {
es, err := v1.Start(dbClient)
if err != nil {
return nil, err
}
view, err := noti_view.StartView(dbClient)
if err != nil {
return nil, err
}
spool := spooler.StartSpooler(conf.Spooler, es, view, dbClient, command, queries, externalPort, externalSecure, dir, assetsPrefix, fileSystemPath, userEncryption, smtpEncryption, smsEncryption)
return &EsRepository{
spool,
}, nil
}
func (repo *EsRepository) Health() error {
return nil
}

View File

@@ -1,20 +0,0 @@
package spooler
import (
"database/sql"
"time"
es_locker "github.com/zitadel/zitadel/internal/eventstore/v1/locker"
)
const (
lockTable = "notification.locks"
)
type locker struct {
dbClient *sql.DB
}
func (l *locker) Renew(lockerID, viewModel, instanceID string, waitTime time.Duration) error {
return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, instanceID, waitTime)
}

View File

@@ -1,47 +0,0 @@
package spooler
import (
"database/sql"
"net/http"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
"github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
"github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/handler"
"github.com/zitadel/zitadel/internal/notification/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/query"
)
type SpoolerConfig struct {
BulkLimit uint64
FailureCountUntilSkip uint64
ConcurrentWorkers int
Handlers handler.Configs
}
func StartSpooler(c SpoolerConfig,
es v1.Eventstore,
view *view.View,
sql *sql.DB,
command *command.Commands,
queries *query.Queries,
externalPort uint16,
externalSecure bool,
dir http.FileSystem,
assetsPrefix,
fileSystemPath string,
userEncryption crypto.EncryptionAlgorithm,
smtpEncryption crypto.EncryptionAlgorithm,
smsEncryption crypto.EncryptionAlgorithm,
) *spooler.Spooler {
spoolerConfig := spooler.Config{
Eventstore: es,
Locker: &locker{dbClient: sql},
ConcurrentWorkers: c.ConcurrentWorkers,
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, command, queries, externalPort, externalSecure, dir, assetsPrefix, fileSystemPath, userEncryption, smtpEncryption, smsEncryption),
}
spool := spoolerConfig.New()
spool.Start()
return spool
}

View File

@@ -1,17 +0,0 @@
package view
import (
"github.com/zitadel/zitadel/internal/view/repository"
)
const (
errTable = "notification.failed_events"
)
func (v *View) saveFailedEvent(failedEvent *repository.FailedEvent) error {
return repository.SaveFailedEvent(v.Db, errTable, failedEvent)
}
func (v *View) latestFailedEvent(viewName, instanceID string, sequence uint64) (*repository.FailedEvent, error) {
return repository.LatestFailedEvent(v.Db, errTable, viewName, instanceID, sequence)
}

View File

@@ -1,34 +0,0 @@
package view
import (
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/view/repository"
)
const (
notificationTable = "notification.notifications"
)
func (v *View) GetLatestNotificationSequence(instanceID string) (*repository.CurrentSequence, error) {
return v.latestSequence(notificationTable, instanceID)
}
func (v *View) GetLatestNotificationSequences() ([]*repository.CurrentSequence, error) {
return v.latestSequences(notificationTable)
}
func (v *View) ProcessedNotificationSequence(event *models.Event) error {
return v.saveCurrentSequence(notificationTable, event)
}
func (v *View) UpdateNotificationSpoolerRunTimestamp() error {
return v.updateSpoolerRunSequence(notificationTable)
}
func (v *View) GetLatestNotificationFailedEvent(sequence uint64, instanceID string) (*repository.FailedEvent, error) {
return v.latestFailedEvent(notificationTable, instanceID, sequence)
}
func (v *View) ProcessedNotificationFailedEvent(failedEvent *repository.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@@ -1,61 +0,0 @@
package view
import (
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/user/repository/view"
"github.com/zitadel/zitadel/internal/user/repository/view/model"
"github.com/zitadel/zitadel/internal/view/repository"
)
const (
notifyUserTable = "notification.notify_users"
)
func (v *View) NotifyUserByID(userID, instanceID string) (*model.NotifyUser, error) {
return view.NotifyUserByID(v.Db, notifyUserTable, userID, instanceID)
}
func (v *View) PutNotifyUser(user *model.NotifyUser, event *models.Event) error {
err := view.PutNotifyUser(v.Db, notifyUserTable, user)
if err != nil {
return err
}
return v.ProcessedNotifyUserSequence(event)
}
func (v *View) NotifyUsersByOrgID(orgID, instanceID string) ([]*model.NotifyUser, error) {
return view.NotifyUsersByOrgID(v.Db, notifyUserTable, orgID, instanceID)
}
func (v *View) DeleteNotifyUser(userID, instanceID string, event *models.Event) error {
err := view.DeleteNotifyUser(v.Db, notifyUserTable, userID, instanceID)
if err != nil && !errors.IsNotFound(err) {
return err
}
return v.ProcessedNotifyUserSequence(event)
}
func (v *View) GetLatestNotifyUserSequence(instanceID string) (*repository.CurrentSequence, error) {
return v.latestSequence(notifyUserTable, instanceID)
}
func (v *View) GetLatestNotifyUserSequences() ([]*repository.CurrentSequence, error) {
return v.latestSequences(notifyUserTable)
}
func (v *View) ProcessedNotifyUserSequence(event *models.Event) error {
return v.saveCurrentSequence(notifyUserTable, event)
}
func (v *View) UpdateNotifyUserSpoolerRunTimestamp() error {
return v.updateSpoolerRunSequence(notifyUserTable)
}
func (v *View) GetLatestNotifyUserFailedEvent(sequence uint64, instanceID string) (*repository.FailedEvent, error) {
return v.latestFailedEvent(notifyUserTable, instanceID, sequence)
}
func (v *View) ProcessedNotifyUserFailedEvent(failedEvent *repository.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@@ -1,38 +0,0 @@
package view
import (
"time"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/view/repository"
)
const (
sequencesTable = "notification.current_sequences"
)
func (v *View) saveCurrentSequence(viewName string, event *models.Event) error {
return repository.SaveCurrentSequence(v.Db, sequencesTable, viewName, event.InstanceID, event.Sequence, event.CreationDate)
}
func (v *View) latestSequence(viewName, instanceID string) (*repository.CurrentSequence, error) {
return repository.LatestSequence(v.Db, sequencesTable, viewName, instanceID)
}
func (v *View) latestSequences(viewName string) ([]*repository.CurrentSequence, error) {
return repository.LatestSequences(v.Db, sequencesTable, viewName)
}
func (v *View) updateSpoolerRunSequence(viewName string) error {
currentSequences, err := repository.LatestSequences(v.Db, sequencesTable, viewName)
if err != nil {
return err
}
for _, currentSequence := range currentSequences {
if currentSequence.ViewName == "" {
currentSequence.ViewName = viewName
}
currentSequence.LastSuccessfulSpoolerRun = time.Now()
}
return repository.UpdateCurrentSequences(v.Db, sequencesTable, currentSequences)
}

View File

@@ -1,25 +0,0 @@
package view
import (
"database/sql"
"github.com/jinzhu/gorm"
)
type View struct {
Db *gorm.DB
}
func StartView(sqlClient *sql.DB) (*View, error) {
gorm, err := gorm.Open("postgres", sqlClient)
if err != nil {
return nil, err
}
return &View{
Db: gorm,
}, nil
}
func (v *View) Health() (err error) {
return v.Db.DB().Ping()
}

View File

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

View File

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

View File

@@ -1,38 +1,17 @@
package types
import (
"context"
"strings"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/templates"
"github.com/zitadel/zitadel/internal/query"
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
)
type DomainClaimedData struct {
templates.TemplateData
URL string
}
func SendDomainClaimed(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, username string, emailConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), colors *query.LabelPolicy, assetsPrefix string, origin string) error {
func (notify Notify) SendDomainClaimed(user *query.NotifyUser, origin, username string) error {
url := login.LoginLink(origin, user.ResourceOwner)
var args = mapNotifyUserToArgs(user)
args := make(map[string]interface{})
args["TempUsername"] = username
args["Domain"] = strings.Split(user.LastEmail, "@")[1]
domainClaimedData := &DomainClaimedData{
TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.DomainClaimedMessageType, user.PreferredLanguage, colors),
URL: url,
}
template, err := templates.GetParsedTemplate(mailhtml, domainClaimedData)
if err != nil {
return err
}
return generateEmail(ctx, user, domainClaimedData.Subject, template, emailConfig, getFileSystemProvider, getLogProvider, true)
return notify(url, args, domain.DomainClaimedMessageType, true)
}

View File

@@ -1,43 +1,14 @@
package types
import (
"context"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/templates"
"github.com/zitadel/zitadel/internal/query"
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
)
type EmailVerificationCodeData struct {
templates.TemplateData
URL string
}
func SendEmailVerificationCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.EmailCode, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string, origin string) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
}
url := login.MailVerificationLink(origin, user.ID, codeString, user.ResourceOwner)
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
emailCodeData := &EmailVerificationCodeData{
TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.VerifyEmailMessageType, user.PreferredLanguage, colors),
URL: url,
}
template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
if err != nil {
return err
}
return generateEmail(ctx, user, emailCodeData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true)
func (notify Notify) SendEmailVerificationCode(user *query.NotifyUser, origin, code string) error {
url := login.MailVerificationLink(origin, user.ID, code, user.ResourceOwner)
args := make(map[string]interface{})
args["Code"] = code
return notify(url, args, domain.VerifyEmailMessageType, true)
}

View File

@@ -1,49 +1,14 @@
package types
import (
"context"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/templates"
"github.com/zitadel/zitadel/internal/query"
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
)
type InitCodeEmailData struct {
templates.TemplateData
URL string
}
type UrlData struct {
UserID string
Code string
PasswordSet bool
OrgID string
}
func SendUserInitCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.InitUserCode, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix, origin string) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
}
url := login.InitUserLink(origin, user.ID, codeString, user.ResourceOwner, user.PasswordSet)
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
initCodeData := &InitCodeEmailData{
TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.InitCodeMessageType, user.PreferredLanguage, colors),
URL: url,
}
template, err := templates.GetParsedTemplate(mailhtml, initCodeData)
if err != nil {
return err
}
return generateEmail(ctx, user, initCodeData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true)
func (notify Notify) SendUserInitCode(user *query.NotifyUser, origin, code string) error {
url := login.InitUserLink(origin, user.ID, code, user.ResourceOwner, user.PasswordSet)
args := make(map[string]interface{})
args["Code"] = code
return notify(url, args, domain.InitCodeMessageType, true)
}

View File

@@ -0,0 +1,73 @@
package types
import (
"context"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
"github.com/zitadel/zitadel/internal/notification/templates"
"github.com/zitadel/zitadel/internal/query"
)
type Notify func(
url string,
args map[string]interface{},
messageType string,
allowUnverifiedNotificationChannel bool,
) error
func SendEmail(
ctx context.Context,
mailhtml string,
translator *i18n.Translator,
user *query.NotifyUser,
emailConfig func(ctx context.Context) (*smtp.EmailConfig, error),
getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error),
getLogProvider func(ctx context.Context) (*log.LogConfig, error),
colors *query.LabelPolicy,
assetsPrefix string,
) Notify {
return func(
url string,
args map[string]interface{},
messageType string,
allowUnverifiedNotificationChannel bool,
) error {
args = mapNotifyUserToArgs(user, args)
data := GetTemplateData(translator, args, assetsPrefix, url, messageType, user.PreferredLanguage.String(), colors)
template, err := templates.GetParsedTemplate(mailhtml, data)
if err != nil {
return err
}
return generateEmail(ctx, user, data.Subject, template, emailConfig, getFileSystemProvider, getLogProvider, allowUnverifiedNotificationChannel)
}
}
func SendSMSTwilio(
ctx context.Context,
translator *i18n.Translator,
user *query.NotifyUser,
twilioConfig func(ctx context.Context) (*twilio.TwilioConfig, error),
getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error),
getLogProvider func(ctx context.Context) (*log.LogConfig, error),
colors *query.LabelPolicy,
assetsPrefix string,
) Notify {
return func(
url string,
args map[string]interface{},
messageType string,
allowUnverifiedNotificationChannel bool,
) error {
args = mapNotifyUserToArgs(user, args)
data := GetTemplateData(translator, args, assetsPrefix, url, messageType, user.PreferredLanguage.String(), colors)
return generateSms(ctx, user, data.Text, twilioConfig, getFileSystemProvider, getLogProvider, allowUnverifiedNotificationChannel)
}
}
func externalLink(origin string) string {
return origin + "/ui/login"
}

View File

@@ -1,51 +1,14 @@
package types
import (
"context"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
"github.com/zitadel/zitadel/internal/notification/templates"
"github.com/zitadel/zitadel/internal/query"
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
)
type PasswordCodeData struct {
templates.TemplateData
FirstName string
LastName string
URL string
}
func SendPasswordCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.PasswordCode, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getTwilioConfig func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string, origin string) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
}
url := login.InitPasswordLink(origin, user.ID, codeString, user.ResourceOwner)
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
passwordResetData := &PasswordCodeData{
TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.PasswordResetMessageType, user.PreferredLanguage, colors),
FirstName: user.FirstName,
LastName: user.LastName,
URL: url,
}
template, err := templates.GetParsedTemplate(mailhtml, passwordResetData)
if err != nil {
return err
}
if code.NotificationType == int32(domain.NotificationTypeSms) {
return generateSms(ctx, user, passwordResetData.Text, getTwilioConfig, getFileSystemProvider, getLogProvider, false)
}
return generateEmail(ctx, user, passwordResetData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true)
func (notify Notify) SendPasswordCode(user *query.NotifyUser, origin, code string) error {
url := login.InitPasswordLink(origin, user.ID, code, user.ResourceOwner)
args := make(map[string]interface{})
args["Code"] = code
return notify(url, args, domain.PasswordResetMessageType, true)
}

View File

@@ -1,42 +1,12 @@
package types
import (
"context"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/templates"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/user"
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
)
type PasswordlessRegistrationLinkData struct {
templates.TemplateData
URL string
}
func SendPasswordlessRegistrationLink(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *user.HumanPasswordlessInitCodeRequestedEvent, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string, origin string) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
}
url := domain.PasswordlessInitCodeLink(origin+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, code.ID, codeString)
var args = mapNotifyUserToArgs(user)
emailCodeData := &PasswordlessRegistrationLinkData{
TemplateData: GetTemplateData(translator, args, assetsPrefix, url, domain.PasswordlessRegistrationMessageType, user.PreferredLanguage, colors),
URL: url,
}
template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
if err != nil {
return err
}
return generateEmail(ctx, user, emailCodeData.Subject, template, smtpConfig, getFileSystemProvider, getLogProvider, true)
func (notify Notify) SendPasswordlessRegistrationLink(user *query.NotifyUser, origin, code, codeID string) error {
url := domain.PasswordlessInitCodeLink(origin+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code)
return notify(url, nil, domain.PasswordlessRegistrationMessageType, true)
}

View File

@@ -1,38 +1,12 @@
package types
import (
"context"
"fmt"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
"github.com/zitadel/zitadel/internal/notification/templates"
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
"github.com/zitadel/zitadel/internal/query"
)
type PhoneVerificationCodeData struct {
UserID string
}
func SendPhoneVerificationCode(ctx context.Context, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.PhoneCode, getTwilioConfig func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
}
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
text := translator.Localize(fmt.Sprintf("%s.%s", domain.VerifyPhoneMessageType, domain.MessageText), args, user.PreferredLanguage)
codeData := &PhoneVerificationCodeData{UserID: user.ID}
template, err := templates.ParseTemplateText(text, codeData)
if err != nil {
return err
}
return generateSms(ctx, user, template, getTwilioConfig, getFileSystemProvider, getLogProvider, true)
func (notify Notify) SendPhoneVerificationCode(user *query.NotifyUser, origin, code string) error {
args := make(map[string]interface{})
args["Code"] = code
return notify("", args, domain.VerifyPhoneMessageType, true)
}

View File

@@ -11,7 +11,7 @@ import (
func GetTemplateData(translator *i18n.Translator, translateArgs map[string]interface{}, assetsPrefix, href, msgType, lang string, policy *query.LabelPolicy) templates.TemplateData {
templateData := templates.TemplateData{
Href: href,
URL: href,
PrimaryColor: templates.DefaultPrimaryColor,
BackgroundColor: templates.DefaultBackgroundColor,
FontColor: templates.DefaultFontColor,

View File

@@ -10,11 +10,10 @@ import (
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/messages"
"github.com/zitadel/zitadel/internal/notification/senders"
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
"github.com/zitadel/zitadel/internal/query"
)
func generateEmail(ctx context.Context, user *view_model.NotifyUser, subject, content string, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastEmail bool) error {
func generateEmail(ctx context.Context, user *query.NotifyUser, subject, content string, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastEmail bool) error {
content = html.UnescapeString(content)
message := &messages.Email{
Recipients: []string{user.VerifiedEmail},
@@ -36,20 +35,22 @@ func generateEmail(ctx context.Context, user *view_model.NotifyUser, subject, co
return channelChain.HandleMessage(message)
}
func mapNotifyUserToArgs(user *view_model.NotifyUser) map[string]interface{} {
return map[string]interface{}{
"UserName": user.UserName,
"FirstName": user.FirstName,
"LastName": user.LastName,
"NickName": user.NickName,
"DisplayName": user.DisplayName,
"LastEmail": user.LastEmail,
"VerifiedEmail": user.VerifiedEmail,
"LastPhone": user.LastPhone,
"VerifiedPhone": user.VerifiedPhone,
"PreferredLoginName": user.PreferredLoginName,
"LoginNames": user.LoginNames,
"ChangeDate": user.ChangeDate,
"CreationDate": user.CreationDate,
func mapNotifyUserToArgs(user *query.NotifyUser, args map[string]interface{}) map[string]interface{} {
if args == nil {
args = make(map[string]interface{})
}
args["UserName"] = user.Username
args["FirstName"] = user.FirstName
args["LastName"] = user.LastName
args["NickName"] = user.NickName
args["DisplayName"] = user.DisplayName
args["LastEmail"] = user.LastEmail
args["VerifiedEmail"] = user.VerifiedEmail
args["LastPhone"] = user.LastPhone
args["VerifiedPhone"] = user.VerifiedPhone
args["PreferredLoginName"] = user.PreferredLoginName
args["LoginNames"] = user.LoginNames
args["ChangeDate"] = user.ChangeDate
args["CreationDate"] = user.CreationDate
return args
}

View File

@@ -3,20 +3,22 @@ package types
import (
"context"
"github.com/zitadel/logging"
caos_errors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/notification/channels/fs"
"github.com/zitadel/zitadel/internal/notification/channels/log"
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
"github.com/zitadel/zitadel/internal/notification/messages"
"github.com/zitadel/zitadel/internal/notification/senders"
view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
"github.com/zitadel/zitadel/internal/query"
)
func generateSms(ctx context.Context, user *view_model.NotifyUser, content string, getTwilioProvider func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastPhone bool) error {
func generateSms(ctx context.Context, user *query.NotifyUser, content string, getTwilioProvider func(ctx context.Context) (*twilio.TwilioConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), lastPhone bool) error {
number := ""
twilio, err := getTwilioProvider(ctx)
twilioConfig, err := getTwilioProvider(ctx)
if err == nil {
number = twilio.SenderNumber
number = twilioConfig.SenderNumber
}
message := &messages.SMS{
SenderPhoneNumber: number,
@@ -27,7 +29,8 @@ func generateSms(ctx context.Context, user *view_model.NotifyUser, content strin
message.RecipientPhoneNumber = user.LastPhone
}
channelChain, err := senders.SMSChannels(ctx, twilio, getFileSystemProvider, getLogProvider)
channelChain, err := senders.SMSChannels(ctx, twilioConfig, getFileSystemProvider, getLogProvider)
logging.OnError(err).Error("could not create sms channel")
if channelChain.Len() == 0 {
return caos_errors.ThrowPreconditionFailed(nil, "PHONE-w8nfow", "Errors.Notification.Channels.NotPresent")