mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:27:42 +00:00
feat: notifications (#109)
* implement notification providers * email provider * notification handler * notify users * implement code sent on user eventstore * send email implementation * send init code * handle codes * fix project member handler * add some logs for debug * send emails * text changes * send sms * notification process * send password code * format phone number * test format phone * remove fmts * remove unused code * rename files * add mocks * merge master * Update internal/notification/providers/email/message.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/notification/repository/eventsourcing/handler/notification.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/notification/repository/eventsourcing/handler/notification.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/notification/providers/email/provider.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * requested changes of mr * move locker to eventstore pkg * Update internal/notification/providers/chat/message.go Co-authored-by: Livio Amstutz <livio.a@gmail.com> * move locker to eventstore pkg * linebreak * Update internal/notification/providers/email/provider.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/notification/repository/eventsourcing/handler/notification.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/notification/repository/eventsourcing/handler/notification.go Co-authored-by: Silvan <silvan.reusser@gmail.com> Co-authored-by: Silvan <silvan.reusser@gmail.com> Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
@@ -1,46 +1,19 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_locker "github.com/caos/zitadel/internal/eventstore/locker"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/cockroach-go/crdb"
|
||||
)
|
||||
|
||||
const (
|
||||
lockTable = "auth.locks"
|
||||
lockedUntilKey = "locked_until"
|
||||
lockerIDKey = "locker_id"
|
||||
objectTypeKey = "object_type"
|
||||
lockTable = "auth.locks"
|
||||
)
|
||||
|
||||
type locker struct {
|
||||
dbClient *sql.DB
|
||||
}
|
||||
|
||||
type lock struct {
|
||||
LockerID string `gorm:"column:locker_id;primary_key"`
|
||||
LockedUntil time.Time `gorm:"column:locked_until"`
|
||||
ViewName string `gorm:"column:object_type;primary_key"`
|
||||
}
|
||||
|
||||
func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error {
|
||||
return crdb.ExecuteTx(context.Background(), l.dbClient, nil, func(tx *sql.Tx) error {
|
||||
query := fmt.Sprintf("INSERT INTO %s (%s, %s, %s) VALUES ($1, $2, now()+$3) ON CONFLICT (%s) DO UPDATE SET %s = now()+$4, %s = $5 WHERE (locks.%s < now() OR locks.%s = $6) AND locks.%s = $7",
|
||||
lockTable, objectTypeKey, lockerIDKey, lockedUntilKey, objectTypeKey, lockedUntilKey, lockerIDKey, lockedUntilKey, lockerIDKey, objectTypeKey)
|
||||
|
||||
rs, err := tx.Exec(query, viewModel, lockerID, waitTime.Seconds(), waitTime.Seconds(), lockerID, lockerID, viewModel)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if rows, _ := rs.RowsAffected(); rows == 0 {
|
||||
tx.Rollback()
|
||||
return caos_errs.ThrowAlreadyExists(nil, "SPOOL-lso0e", "view already locked")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, waitTime)
|
||||
}
|
||||
|
@@ -3,6 +3,10 @@ package systemdefaults
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/notification/providers/chat"
|
||||
"github.com/caos/zitadel/internal/notification/providers/email"
|
||||
"github.com/caos/zitadel/internal/notification/providers/twilio"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
pol "github.com/caos/zitadel/internal/policy"
|
||||
)
|
||||
|
||||
@@ -14,6 +18,7 @@ type SystemDefaults struct {
|
||||
DefaultPolicies DefaultPolicies
|
||||
IamID string
|
||||
SetUp types.IAMSetUp
|
||||
Notifications Notifications
|
||||
}
|
||||
|
||||
type SecretGenerators struct {
|
||||
@@ -46,3 +51,29 @@ type DefaultPolicies struct {
|
||||
Complexity pol.PasswordComplexityPolicyDefault
|
||||
Lockout pol.PasswordLockoutPolicyDefault
|
||||
}
|
||||
|
||||
type Notifications struct {
|
||||
DebugMode bool
|
||||
Endpoints Endpoints
|
||||
Providers Providers
|
||||
TemplateData TemplateData
|
||||
}
|
||||
|
||||
type Endpoints struct {
|
||||
InitCode string
|
||||
PasswordReset string
|
||||
VerifyEmail string
|
||||
}
|
||||
|
||||
type Providers struct {
|
||||
Chat chat.ChatConfig
|
||||
Email email.EmailConfig
|
||||
Twilio twilio.TwilioConfig
|
||||
}
|
||||
|
||||
type TemplateData struct {
|
||||
InitCode templates.TemplateData
|
||||
PasswordReset templates.TemplateData
|
||||
VerifyEmail templates.TemplateData
|
||||
VerifyPhone templates.TemplateData
|
||||
}
|
||||
|
40
internal/eventstore/locker/lock.go
Normal file
40
internal/eventstore/locker/lock.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package locker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/cockroachdb/cockroach-go/crdb"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
lockedUntilKey = "locked_until"
|
||||
lockerIDKey = "locker_id"
|
||||
objectTypeKey = "object_type"
|
||||
)
|
||||
|
||||
type lock struct {
|
||||
LockerID string `gorm:"column:locker_id;primary_key"`
|
||||
LockedUntil time.Time `gorm:"column:locked_until"`
|
||||
ViewName string `gorm:"column:object_type;primary_key"`
|
||||
}
|
||||
|
||||
func Renew(dbClient *sql.DB, lockTable, lockerID, viewModel string, waitTime time.Duration) error {
|
||||
return crdb.ExecuteTx(context.Background(), dbClient, nil, func(tx *sql.Tx) error {
|
||||
query := fmt.Sprintf("INSERT INTO %s (%s, %s, %s) VALUES ($1, $2, now()+$3) ON CONFLICT (%s) DO UPDATE SET %s = now()+$4, %s = $5 WHERE (locks.%s < now() OR locks.%s = $6) AND locks.%s = $7",
|
||||
lockTable, objectTypeKey, lockerIDKey, lockedUntilKey, objectTypeKey, lockedUntilKey, lockerIDKey, lockedUntilKey, lockerIDKey, objectTypeKey)
|
||||
|
||||
rs, err := tx.Exec(query, viewModel, lockerID, waitTime.Seconds(), waitTime.Seconds(), lockerID, lockerID, viewModel)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if rows, _ := rs.RowsAffected(); rows == 0 {
|
||||
tx.Rollback()
|
||||
return caos_errs.ThrowAlreadyExists(nil, "SPOOL-lso0e", "view already locked")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
125
internal/eventstore/locker/lock_test.go
Normal file
125
internal/eventstore/locker/lock_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package locker
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
type dbMock struct {
|
||||
db *sql.DB
|
||||
mock sqlmock.Sqlmock
|
||||
}
|
||||
|
||||
func mockDB(t *testing.T) *dbMock {
|
||||
mockDB := dbMock{}
|
||||
var err error
|
||||
mockDB.db, mockDB.mock, err = sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("error occured while creating stub db %v", err)
|
||||
}
|
||||
|
||||
mockDB.mock.MatchExpectationsInOrder(true)
|
||||
|
||||
return &mockDB
|
||||
}
|
||||
|
||||
func (db *dbMock) expectCommit() *dbMock {
|
||||
db.mock.ExpectCommit()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectRollback() *dbMock {
|
||||
db.mock.ExpectRollback()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectBegin() *dbMock {
|
||||
db.mock.ExpectBegin()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectSavepoint() *dbMock {
|
||||
db.mock.ExpectExec("SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectReleaseSavepoint() *dbMock {
|
||||
db.mock.ExpectExec("RELEASE SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectRenew(lockerID, view string, affectedRows int64) *dbMock {
|
||||
query := db.mock.
|
||||
ExpectExec(`INSERT INTO table\.locks \(object_type, locker_id, locked_until\) VALUES \(\$1, \$2, now\(\)\+\$3\) ON CONFLICT \(object_type\) DO UPDATE SET locked_until = now\(\)\+\$4, locker_id = \$5 WHERE \(locks\.locked_until < now\(\) OR locks\.locker_id = \$6\) AND locks\.object_type = \$7`).
|
||||
WithArgs(view, lockerID, sqlmock.AnyArg(), sqlmock.AnyArg(), lockerID, lockerID, view).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
if affectedRows == 0 {
|
||||
query.WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
} else {
|
||||
query.WillReturnResult(sqlmock.NewResult(1, affectedRows))
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func Test_locker_Renew(t *testing.T) {
|
||||
type fields struct {
|
||||
db *dbMock
|
||||
}
|
||||
type args struct {
|
||||
tableName string
|
||||
lockerID string
|
||||
viewModel string
|
||||
waitTime time.Duration
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "renew succeeded",
|
||||
fields: fields{
|
||||
db: mockDB(t).
|
||||
expectBegin().
|
||||
expectSavepoint().
|
||||
expectRenew("locker", "view", 1).
|
||||
expectReleaseSavepoint().
|
||||
expectCommit(),
|
||||
},
|
||||
args: args{tableName: "table.locks", lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "renew now rows updated",
|
||||
fields: fields{
|
||||
db: mockDB(t).
|
||||
expectBegin().
|
||||
expectSavepoint().
|
||||
expectRenew("locker", "view", 0).
|
||||
expectRollback(),
|
||||
},
|
||||
args: args{tableName: "table.locks", lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := Renew(tt.fields.db.db, tt.args.tableName, tt.args.lockerID, tt.args.viewModel, tt.args.waitTime); (err != nil) != tt.wantErr {
|
||||
t.Errorf("locker.Renew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err := tt.fields.db.mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("not all database expectations met: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/management/repository/eventsourcing/view"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
type Configs map[string]*Config
|
||||
|
||||
type Config struct {
|
||||
MinimumCycleDurationMillisecond int
|
||||
MinimumCycleDuration types.Duration
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
@@ -31,8 +32,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, ev
|
||||
return []spooler.Handler{
|
||||
&GrantedProject{handler: handler{view, bulkLimit, configs.cycleDuration("GrantedProject"), errorCount}, eventstore: eventstore, projectEvents: repos.ProjectEvents},
|
||||
&ProjectRole{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectRole"), errorCount}, projectEvents: repos.ProjectEvents},
|
||||
&ProjectMember{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectMember"), errorCount}},
|
||||
&ProjectGrantMember{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectGrantMember"), errorCount}},
|
||||
&ProjectMember{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectMember"), errorCount}, userEvents: repos.UserEvents},
|
||||
&ProjectGrantMember{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectGrantMember"), errorCount}, userEvents: repos.UserEvents},
|
||||
&Application{handler: handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount}},
|
||||
&User{handler: handler{view, bulkLimit, configs.cycleDuration("User"), errorCount}},
|
||||
&UserGrant{handler: handler{view, bulkLimit, configs.cycleDuration("UserGrant"), errorCount}, projectEvents: repos.ProjectEvents, userEvents: repos.UserEvents},
|
||||
@@ -44,5 +45,5 @@ func (configs Configs) cycleDuration(viewModel string) time.Duration {
|
||||
if !ok {
|
||||
return 1 * time.Second
|
||||
}
|
||||
return time.Duration(c.MinimumCycleDurationMillisecond) * time.Millisecond
|
||||
return c.MinimumCycleDuration.Duration
|
||||
}
|
||||
|
@@ -79,7 +79,7 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
|
||||
}
|
||||
org := es_org.StartOrg(es_org.OrgConfig{Eventstore: es})
|
||||
|
||||
eventstoreRepos := handler.EventstoreRepos{ProjectEvents: project}
|
||||
eventstoreRepos := handler.EventstoreRepos{ProjectEvents: project, UserEvents: user}
|
||||
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, eventstoreRepos)
|
||||
|
||||
return &EsRepository{
|
||||
|
@@ -1,46 +1,19 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_locker "github.com/caos/zitadel/internal/eventstore/locker"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/cockroach-go/crdb"
|
||||
)
|
||||
|
||||
const (
|
||||
lockTable = "management.locks"
|
||||
lockedUntilKey = "locked_until"
|
||||
lockerIDKey = "locker_id"
|
||||
objectTypeKey = "object_type"
|
||||
lockTable = "management.locks"
|
||||
)
|
||||
|
||||
type locker struct {
|
||||
dbClient *sql.DB
|
||||
}
|
||||
|
||||
type lock struct {
|
||||
LockerID string `gorm:"column:locker_id;primary_key"`
|
||||
LockedUntil time.Time `gorm:"column:locked_until"`
|
||||
ViewName string `gorm:"column:object_type;primary_key"`
|
||||
}
|
||||
|
||||
func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error {
|
||||
return crdb.ExecuteTx(context.Background(), l.dbClient, nil, func(tx *sql.Tx) error {
|
||||
query := fmt.Sprintf("INSERT INTO %s (%s, %s, %s) VALUES ($1, $2, now()+$3) ON CONFLICT (%s) DO UPDATE SET %s = now()+$4, %s = $5 WHERE (locks.%s < now() OR locks.%s = $6) AND locks.%s = $7",
|
||||
lockTable, objectTypeKey, lockerIDKey, lockedUntilKey, objectTypeKey, lockedUntilKey, lockerIDKey, lockedUntilKey, lockerIDKey, objectTypeKey)
|
||||
|
||||
rs, err := tx.Exec(query, viewModel, lockerID, waitTime.Seconds(), waitTime.Seconds(), lockerID, lockerID, viewModel)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if rows, _ := rs.RowsAffected(); rows == 0 {
|
||||
tx.Rollback()
|
||||
return caos_errs.ThrowAlreadyExists(nil, "SPOOL-lso0e", "view already locked")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, waitTime)
|
||||
}
|
||||
|
17
internal/notification/notification.go
Normal file
17
internal/notification/notification.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/logging"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Repository eventsourcing.Config
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, config Config, systemDefaults sd.SystemDefaults) {
|
||||
_, err := eventsourcing.Start(config.Repository, systemDefaults)
|
||||
logging.Log("MAIN-9uBxp").OnError(err).Panic("unable to start app")
|
||||
}
|
6
internal/notification/providers/chat/config.go
Normal file
6
internal/notification/providers/chat/config.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package chat
|
||||
|
||||
type ChatConfig struct {
|
||||
Url string
|
||||
SplitCount int
|
||||
}
|
9
internal/notification/providers/chat/message.go
Normal file
9
internal/notification/providers/chat/message.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package chat
|
||||
|
||||
type ChatMessage struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (msg *ChatMessage) GetContent() string {
|
||||
return msg.Text
|
||||
}
|
75
internal/notification/providers/chat/provider.go
Normal file
75
internal/notification/providers/chat/provider.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type Chat struct {
|
||||
URL *url.URL
|
||||
SplitCount int
|
||||
}
|
||||
|
||||
func InitChatProvider(config ChatConfig) (*Chat, error) {
|
||||
url, err := url.Parse(config.Url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Chat{
|
||||
URL: url,
|
||||
SplitCount: config.SplitCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (chat *Chat) CanHandleMessage(_ providers.Message) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (chat *Chat) HandleMessage(message providers.Message) error {
|
||||
contentText := message.GetContent()
|
||||
for _, splittedMsg := range splitMessage(contentText, chat.SplitCount) {
|
||||
chatMsg := &ChatMessage{Text: splittedMsg}
|
||||
if err := chat.SendMessage(chatMsg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (chat *Chat) SendMessage(message providers.Message) error {
|
||||
chatMsg, ok := message.(*ChatMessage)
|
||||
if !ok {
|
||||
return caos_errs.ThrowInternal(nil, "EMAIL-s8JLs", "message is not ChatMessage")
|
||||
}
|
||||
req, err := json.Marshal(chatMsg)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "PROVI-s8uie", "Could not unmarshal content")
|
||||
}
|
||||
|
||||
_, err = http.Post(chat.URL.String(), "application/json; charset=UTF-8", bytes.NewReader(req))
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "PROVI-si93s", "unable to send message")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitMessage(message string, count int) []string {
|
||||
if count == 0 {
|
||||
return []string{message}
|
||||
}
|
||||
var splits []string
|
||||
var l, r int
|
||||
for l, r = 0, count; r < len(message); l, r = r, r+count {
|
||||
for !utf8.RuneStart(message[r]) {
|
||||
r--
|
||||
}
|
||||
splits = append(splits, message[l:r])
|
||||
}
|
||||
splits = append(splits, message[l:])
|
||||
return splits
|
||||
}
|
18
internal/notification/providers/email/config.go
Normal file
18
internal/notification/providers/email/config.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package email
|
||||
|
||||
type EmailConfig struct {
|
||||
SMTP SMTP
|
||||
Tls bool
|
||||
From string
|
||||
FromName string
|
||||
}
|
||||
|
||||
type SMTP struct {
|
||||
Host string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (smtp *SMTP) HasAuth() bool {
|
||||
return smtp.User != "" && smtp.Password != ""
|
||||
}
|
47
internal/notification/providers/email/message.go
Normal file
47
internal/notification/providers/email/message.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
isHTMLRgx = regexp.MustCompile(`.*<html.*>.*`)
|
||||
lineBreak = "\r\n"
|
||||
)
|
||||
|
||||
type EmailMessage struct {
|
||||
Recipients []string
|
||||
BCC []string
|
||||
CC []string
|
||||
SenderEmail string
|
||||
Subject string
|
||||
Content string
|
||||
}
|
||||
|
||||
func (msg *EmailMessage) GetContent() string {
|
||||
headers := make(map[string]string)
|
||||
headers["From"] = msg.SenderEmail
|
||||
headers["To"] = strings.Join(msg.Recipients, ", ")
|
||||
headers["Cc"] = strings.Join(msg.CC, ", ")
|
||||
|
||||
message := ""
|
||||
for k, v := range headers {
|
||||
message += fmt.Sprintf("%s: %s"+lineBreak, k, v)
|
||||
}
|
||||
|
||||
//default mime-type is html
|
||||
mime := "MIME-version: 1.0;" + lineBreak + "Content-Type: text/html; charset=\"UTF-8\";" + lineBreak + lineBreak
|
||||
if !isHTML(msg.Content) {
|
||||
mime = "MIME-version: 1.0;" + lineBreak + "Content-Type: text/plain; charset=\"UTF-8\";" + lineBreak + lineBreak
|
||||
}
|
||||
subject := "Subject: " + msg.Subject + lineBreak
|
||||
message += subject + mime + lineBreak + msg.Content
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
func isHTML(input string) bool {
|
||||
return isHTMLRgx.MatchString(input)
|
||||
}
|
123
internal/notification/providers/email/provider.go
Normal file
123
internal/notification/providers/email/provider.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
"net"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
type Email struct {
|
||||
smtpClient *smtp.Client
|
||||
}
|
||||
|
||||
func InitEmailProvider(config EmailConfig) (*Email, error) {
|
||||
client, err := config.SMTP.connectToSMTP(config.Tls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Email{
|
||||
smtpClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (email *Email) CanHandleMessage(message providers.Message) bool {
|
||||
msg, ok := message.(*EmailMessage)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return msg.Content != "" && msg.Subject != "" && len(msg.Recipients) > 0
|
||||
}
|
||||
|
||||
func (email *Email) HandleMessage(message providers.Message) error {
|
||||
defer email.smtpClient.Close()
|
||||
emailMsg, ok := message.(*EmailMessage)
|
||||
if !ok {
|
||||
return caos_errs.ThrowInternal(nil, "EMAIL-s8JLs", "message is not EmailMessage")
|
||||
}
|
||||
// To && From
|
||||
if err := email.smtpClient.Mail(emailMsg.SenderEmail); err != nil {
|
||||
return caos_errs.ThrowInternalf(err, "EMAIL-s3is3", "could not set sender: %v", emailMsg.SenderEmail)
|
||||
}
|
||||
|
||||
for _, recp := range append(append(emailMsg.Recipients, emailMsg.CC...), emailMsg.BCC...) {
|
||||
if err := email.smtpClient.Rcpt(recp); err != nil {
|
||||
return caos_errs.ThrowInternalf(err, "EMAIL-s4is4", "could not set recipient: %v", recp)
|
||||
}
|
||||
}
|
||||
|
||||
// Data
|
||||
w, err := email.smtpClient.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(emailMsg.GetContent()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer logging.LogWithFields("EMAI-a1c87ec8").Debug("email sent")
|
||||
return email.smtpClient.Quit()
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) connectToSMTP(tlsRequired bool) (client *smtp.Client, err error) {
|
||||
host, _, err := net.SplitHostPort(smtpConfig.Host)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "EMAIL-spR56", "could not split host and port for connect to smtp")
|
||||
}
|
||||
|
||||
if !tlsRequired {
|
||||
client, err = smtpConfig.getSMPTClient()
|
||||
} else {
|
||||
client, err = smtpConfig.getSMPTClientWithTls(host)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = smtpConfig.smtpAuth(client, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) getSMPTClient() (*smtp.Client, error) {
|
||||
client, err := smtp.Dial(smtpConfig.Host)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "EMAIL-skwos", "Could not make smtp dial")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) getSMPTClientWithTls(host string) (*smtp.Client, error) {
|
||||
conn, err := tls.Dial("tcp", smtpConfig.Host, &tls.Config{})
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "EMAIL-sl39s", "Could not make tls dial")
|
||||
}
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "EMAIL-skwi4", "Could not create smtp client")
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) smtpAuth(client *smtp.Client, host string) error {
|
||||
if !smtpConfig.HasAuth() {
|
||||
return nil
|
||||
}
|
||||
// Auth
|
||||
auth := smtp.PlainAuth("", smtpConfig.User, smtpConfig.Password, host)
|
||||
err := client.Auth(auth)
|
||||
logging.Log("EMAIL-s9kfs").WithField("smtp user", smtpConfig.User).OnError(err).Debug("Could not add smtp auth")
|
||||
return err
|
||||
}
|
4
internal/notification/providers/gen_mock.go
Normal file
4
internal/notification/providers/gen_mock.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package providers
|
||||
|
||||
//go:generate mockgen -package mock -destination ./mock/provider.mock.go github.com/caos/zitadel/internal/notification/providers NotificationProvider
|
||||
//go:generate mockgen -package mock -destination ./mock/message.mock.go github.com/caos/zitadel/internal/notification/providers Message
|
5
internal/notification/providers/message.go
Normal file
5
internal/notification/providers/message.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package providers
|
||||
|
||||
type Message interface {
|
||||
GetContent() string
|
||||
}
|
47
internal/notification/providers/mock/message.mock.go
Normal file
47
internal/notification/providers/mock/message.mock.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/caos/zitadel/internal/notification/providers (interfaces: Message)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockMessage is a mock of Message interface
|
||||
type MockMessage struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockMessageMockRecorder
|
||||
}
|
||||
|
||||
// MockMessageMockRecorder is the mock recorder for MockMessage
|
||||
type MockMessageMockRecorder struct {
|
||||
mock *MockMessage
|
||||
}
|
||||
|
||||
// NewMockMessage creates a new mock instance
|
||||
func NewMockMessage(ctrl *gomock.Controller) *MockMessage {
|
||||
mock := &MockMessage{ctrl: ctrl}
|
||||
mock.recorder = &MockMessageMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockMessage) EXPECT() *MockMessageMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetContent mocks base method
|
||||
func (m *MockMessage) GetContent() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetContent")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetContent indicates an expected call of GetContent
|
||||
func (mr *MockMessageMockRecorder) GetContent() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContent", reflect.TypeOf((*MockMessage)(nil).GetContent))
|
||||
}
|
61
internal/notification/providers/mock/provider.mock.go
Normal file
61
internal/notification/providers/mock/provider.mock.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/caos/zitadel/internal/notification/providers (interfaces: NotificationProvider)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockNotificationProvider is a mock of NotificationProvider interface
|
||||
type MockNotificationProvider struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockNotificationProviderMockRecorder
|
||||
}
|
||||
|
||||
// MockNotificationProviderMockRecorder is the mock recorder for MockNotificationProvider
|
||||
type MockNotificationProviderMockRecorder struct {
|
||||
mock *MockNotificationProvider
|
||||
}
|
||||
|
||||
// NewMockNotificationProvider creates a new mock instance
|
||||
func NewMockNotificationProvider(ctrl *gomock.Controller) *MockNotificationProvider {
|
||||
mock := &MockNotificationProvider{ctrl: ctrl}
|
||||
mock.recorder = &MockNotificationProviderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockNotificationProvider) EXPECT() *MockNotificationProviderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CanHandleMessage mocks base method
|
||||
func (m *MockNotificationProvider) CanHandleMessage() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CanHandleMessage")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CanHandleMessage indicates an expected call of CanHandleMessage
|
||||
func (mr *MockNotificationProviderMockRecorder) CanHandleMessage() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanHandleMessage", reflect.TypeOf((*MockNotificationProvider)(nil).CanHandleMessage))
|
||||
}
|
||||
|
||||
// HandleMessage mocks base method
|
||||
func (m *MockNotificationProvider) HandleMessage() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HandleMessage")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HandleMessage indicates an expected call of HandleMessage
|
||||
func (mr *MockNotificationProviderMockRecorder) HandleMessage() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleMessage", reflect.TypeOf((*MockNotificationProvider)(nil).HandleMessage))
|
||||
}
|
6
internal/notification/providers/provider.go
Normal file
6
internal/notification/providers/provider.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package providers
|
||||
|
||||
type NotificationProvider interface {
|
||||
CanHandleMessage() bool
|
||||
HandleMessage() error
|
||||
}
|
7
internal/notification/providers/twilio/config.go
Normal file
7
internal/notification/providers/twilio/config.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package twilio
|
||||
|
||||
type TwilioConfig struct {
|
||||
SID string
|
||||
Token string
|
||||
From string
|
||||
}
|
11
internal/notification/providers/twilio/message.go
Normal file
11
internal/notification/providers/twilio/message.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package twilio
|
||||
|
||||
type TwilioMessage struct {
|
||||
SenderPhoneNumber string
|
||||
RecipientPhoneNumber string
|
||||
Content string
|
||||
}
|
||||
|
||||
func (msg *TwilioMessage) GetContent() string {
|
||||
return msg.Content
|
||||
}
|
39
internal/notification/providers/twilio/provider.go
Normal file
39
internal/notification/providers/twilio/provider.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package twilio
|
||||
|
||||
import (
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
twilio "github.com/kevinburke/twilio-go"
|
||||
)
|
||||
|
||||
type Twilio struct {
|
||||
client *twilio.Client
|
||||
}
|
||||
|
||||
func InitTwilioProvider(config TwilioConfig) *Twilio {
|
||||
return &Twilio{
|
||||
client: twilio.NewClient(config.SID, config.Token, nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Twilio) CanHandleMessage(message providers.Message) bool {
|
||||
twilioMsg, ok := message.(*TwilioMessage)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return twilioMsg.Content != "" && twilioMsg.RecipientPhoneNumber != "" && twilioMsg.SenderPhoneNumber != ""
|
||||
}
|
||||
|
||||
func (t *Twilio) HandleMessage(message providers.Message) error {
|
||||
twilioMsg, ok := message.(*TwilioMessage)
|
||||
if !ok {
|
||||
return caos_errs.ThrowInternal(nil, "TWILI-s0pLc", "message is not TwilioMessage")
|
||||
}
|
||||
m, err := t.client.Messages.SendMessage(twilioMsg.SenderPhoneNumber, twilioMsg.RecipientPhoneNumber, twilioMsg.GetContent(), nil)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "TWILI-osk3S", "could not send message")
|
||||
}
|
||||
logging.LogWithFields("SMS_-f335c523", "message_sid", m.Sid, "status", m.Status).Debug("sms sent")
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/logging"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
|
||||
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Configs map[string]*Config
|
||||
|
||||
type Config struct {
|
||||
MinimumCycleDuration types.Duration
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
view *view.View
|
||||
bulkLimit uint64
|
||||
cycleDuration time.Duration
|
||||
errorCountUntilSkip uint64
|
||||
}
|
||||
|
||||
type EventstoreRepos struct {
|
||||
UserEvents *usr_event.UserEventstore
|
||||
}
|
||||
|
||||
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, eventstore eventstore.Eventstore, repos EventstoreRepos, systemDefaults sd.SystemDefaults) []spooler.Handler {
|
||||
aesCrypto, err := crypto.NewAESCrypto(systemDefaults.UserVerificationKey)
|
||||
if err != nil {
|
||||
logging.Log("HANDL-s90ew").WithError(err).Debug("error create new aes crypto")
|
||||
}
|
||||
return []spooler.Handler{
|
||||
&NotifyUser{handler: handler{view, bulkLimit, configs.cycleDuration("User"), errorCount}},
|
||||
&Notification{handler: handler{view, bulkLimit, configs.cycleDuration("Notification"), errorCount}, eventstore: eventstore, userEvents: repos.UserEvents, systemDefaults: systemDefaults, AesCrypto: aesCrypto},
|
||||
}
|
||||
}
|
||||
|
||||
func (configs Configs) cycleDuration(viewModel string) time.Duration {
|
||||
c, ok := configs[viewModel]
|
||||
if !ok {
|
||||
return 1 * time.Second
|
||||
}
|
||||
return c.MinimumCycleDuration.Duration
|
||||
}
|
@@ -0,0 +1,168 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/api/auth"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/notification/types"
|
||||
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
handler
|
||||
eventstore eventstore.Eventstore
|
||||
userEvents *usr_event.UserEventstore
|
||||
systemDefaults sd.SystemDefaults
|
||||
AesCrypto crypto.EncryptionAlgorithm
|
||||
}
|
||||
|
||||
const (
|
||||
notificationTable = "notification.notifications"
|
||||
NOTIFY_USER = "NOTIFICATION"
|
||||
)
|
||||
|
||||
func (n *Notification) MinimumCycleDuration() time.Duration { return n.cycleDuration }
|
||||
|
||||
func (n *Notification) ViewModel() string {
|
||||
return notificationTable
|
||||
}
|
||||
|
||||
func (n *Notification) EventQuery() (*models.SearchQuery, error) {
|
||||
sequence, err := n.view.GetLatestNotificationSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return eventsourcing.UserQuery(sequence), nil
|
||||
}
|
||||
|
||||
func (n *Notification) Process(event *models.Event) (err error) {
|
||||
switch event.Type {
|
||||
case es_model.InitializedUserCodeAdded:
|
||||
err = n.handleInitUserCode(event)
|
||||
case es_model.UserEmailCodeAdded:
|
||||
err = n.handleEmailVerificationCode(event)
|
||||
case es_model.UserPhoneCodeAdded:
|
||||
err = n.handlePhoneVerificationCode(event)
|
||||
case es_model.UserPasswordCodeAdded:
|
||||
err = n.handlePasswordCode(event)
|
||||
default:
|
||||
return n.view.ProcessedNotificationSequence(event.Sequence)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.view.ProcessedNotificationSequence(event.Sequence)
|
||||
}
|
||||
|
||||
func (n *Notification) handleInitUserCode(event *models.Event) (err error) {
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandled(event.AggregateID, event.Sequence, es_model.InitializedUserCodeAdded, es_model.InitializedUserCodeSent)
|
||||
if err != nil || alreadyHandled {
|
||||
return err
|
||||
}
|
||||
initCode := new(es_model.InitUserCode)
|
||||
initCode.SetData(event)
|
||||
user, err := n.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendUserInitCode(user, initCode, n.systemDefaults, n.AesCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.userEvents.InitCodeSent(getSetNotifyContextData(event.ResourceOwner), event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handlePasswordCode(event *models.Event) (err error) {
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandled(event.AggregateID, event.Sequence, es_model.UserPasswordCodeAdded, es_model.UserPasswordCodeSent)
|
||||
if err != nil || alreadyHandled {
|
||||
return err
|
||||
}
|
||||
pwCode := new(es_model.PasswordCode)
|
||||
pwCode.SetData(event)
|
||||
user, err := n.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendPasswordCode(user, pwCode, n.systemDefaults, n.AesCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.userEvents.PasswordCodeSent(getSetNotifyContextData(event.ResourceOwner), event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handleEmailVerificationCode(event *models.Event) (err error) {
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandled(event.AggregateID, event.Sequence, es_model.UserEmailCodeAdded, es_model.UserEmailCodeSent)
|
||||
if err != nil || alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
emailCode := new(es_model.EmailCode)
|
||||
emailCode.SetData(event)
|
||||
user, err := n.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendEmailVerificationCode(user, emailCode, n.systemDefaults, n.AesCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.userEvents.EmailVerificationCodeSent(getSetNotifyContextData(event.ResourceOwner), event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handlePhoneVerificationCode(event *models.Event) (err error) {
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandled(event.AggregateID, event.Sequence, es_model.UserPhoneCodeAdded, es_model.UserPhoneCodeSent)
|
||||
if err != nil || alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
phoneCode := new(es_model.PhoneCode)
|
||||
phoneCode.SetData(event)
|
||||
user, err := n.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendPhoneVerificationCode(user, phoneCode, n.systemDefaults, n.AesCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.userEvents.PhoneVerificationCodeSent(getSetNotifyContextData(event.ResourceOwner), event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) checkIfCodeAlreadyHandled(userID string, sequence uint64, addedType, sentType models.EventType) (bool, error) {
|
||||
events, err := n.getUserEvents(userID, sequence)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, event := range events {
|
||||
if event.Type == addedType || event.Type == sentType {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (n *Notification) getUserEvents(userID string, sequence uint64) ([]*models.Event, error) {
|
||||
query, err := eventsourcing.UserByIDQuery(userID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return n.eventstore.FilterEvents(context.Background(), query)
|
||||
}
|
||||
|
||||
func (n *Notification) OnError(event *models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-s9opc", "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 getSetNotifyContextData(orgID string) context.Context {
|
||||
return auth.SetCtxData(context.Background(), auth.CtxData{UserID: NOTIFY_USER, OrgID: orgID})
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type NotifyUser struct {
|
||||
handler
|
||||
eventstore eventstore.Eventstore
|
||||
}
|
||||
|
||||
const (
|
||||
userTable = "notification.notify_users"
|
||||
)
|
||||
|
||||
func (p *NotifyUser) MinimumCycleDuration() time.Duration { return p.cycleDuration }
|
||||
|
||||
func (p *NotifyUser) ViewModel() string {
|
||||
return userTable
|
||||
}
|
||||
|
||||
func (p *NotifyUser) EventQuery() (*models.SearchQuery, error) {
|
||||
sequence, err := p.view.GetLatestNotifyUserSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return eventsourcing.UserQuery(sequence), nil
|
||||
}
|
||||
|
||||
func (p *NotifyUser) Process(event *models.Event) (err error) {
|
||||
user := new(view_model.NotifyUser)
|
||||
switch event.Type {
|
||||
case es_model.UserAdded,
|
||||
es_model.UserRegistered:
|
||||
user.AppendEvent(event)
|
||||
case es_model.UserProfileChanged,
|
||||
es_model.UserEmailChanged,
|
||||
es_model.UserEmailVerified,
|
||||
es_model.UserPhoneChanged,
|
||||
es_model.UserPhoneVerified:
|
||||
user, err = p.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = user.AppendEvent(event)
|
||||
case es_model.UserDeleted:
|
||||
err = p.view.DeleteNotifyUser(event.AggregateID, event.Sequence)
|
||||
default:
|
||||
return p.view.ProcessedNotifyUserSequence(event.Sequence)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.view.PutNotifyUser(user)
|
||||
}
|
||||
|
||||
func (p *NotifyUser) OnError(event *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)
|
||||
}
|
56
internal/notification/repository/eventsourcing/repository.go
Normal file
56
internal/notification/repository/eventsourcing/repository.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
es_int "github.com/caos/zitadel/internal/eventstore"
|
||||
es_spol "github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/handler"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/spooler"
|
||||
noti_view "github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
|
||||
es_usr "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Eventstore es_int.Config
|
||||
View types.SQL
|
||||
Spooler spooler.SpoolerConfig
|
||||
}
|
||||
|
||||
type EsRepository struct {
|
||||
spooler *es_spol.Spooler
|
||||
}
|
||||
|
||||
func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error) {
|
||||
es, err := es_int.Start(conf.Eventstore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlClient, err := conf.View.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, err := noti_view.StartView(sqlClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := es_usr.StartUser(es_usr.UserConfig{
|
||||
Eventstore: es,
|
||||
Cache: conf.Eventstore.Cache,
|
||||
}, systemDefaults)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eventstoreRepos := handler.EventstoreRepos{UserEvents: user}
|
||||
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, eventstoreRepos, systemDefaults)
|
||||
|
||||
return &EsRepository{
|
||||
spool,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *EsRepository) Health() error {
|
||||
return nil
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
es_locker "github.com/caos/zitadel/internal/eventstore/locker"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
lockTable = "notification.locks"
|
||||
)
|
||||
|
||||
type locker struct {
|
||||
dbClient *sql.DB
|
||||
}
|
||||
|
||||
func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error {
|
||||
return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, waitTime)
|
||||
}
|
@@ -0,0 +1,127 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
type dbMock struct {
|
||||
db *sql.DB
|
||||
mock sqlmock.Sqlmock
|
||||
}
|
||||
|
||||
func mockDB(t *testing.T) *dbMock {
|
||||
mockDB := dbMock{}
|
||||
var err error
|
||||
mockDB.db, mockDB.mock, err = sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("error occured while creating stub db %v", err)
|
||||
}
|
||||
|
||||
mockDB.mock.MatchExpectationsInOrder(true)
|
||||
|
||||
return &mockDB
|
||||
}
|
||||
|
||||
func (db *dbMock) expectCommit() *dbMock {
|
||||
db.mock.ExpectCommit()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectRollback() *dbMock {
|
||||
db.mock.ExpectRollback()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectBegin() *dbMock {
|
||||
db.mock.ExpectBegin()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectSavepoint() *dbMock {
|
||||
db.mock.ExpectExec("SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectReleaseSavepoint() *dbMock {
|
||||
db.mock.ExpectExec("RELEASE SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectRenew(lockerID, view string, affectedRows int64) *dbMock {
|
||||
query := db.mock.
|
||||
ExpectExec(`INSERT INTO notification\.locks \(object_type, locker_id, locked_until\) VALUES \(\$1, \$2, now\(\)\+\$3\) ON CONFLICT \(object_type\) DO UPDATE SET locked_until = now\(\)\+\$4, locker_id = \$5 WHERE \(locks\.locked_until < now\(\) OR locks\.locker_id = \$6\) AND locks\.object_type = \$7`).
|
||||
WithArgs(view, lockerID, sqlmock.AnyArg(), sqlmock.AnyArg(), lockerID, lockerID, view).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
if affectedRows == 0 {
|
||||
query.WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
} else {
|
||||
query.WillReturnResult(sqlmock.NewResult(1, affectedRows))
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func Test_locker_Renew(t *testing.T) {
|
||||
type fields struct {
|
||||
db *dbMock
|
||||
}
|
||||
type args struct {
|
||||
lockerID string
|
||||
viewModel string
|
||||
waitTime time.Duration
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "renew succeeded",
|
||||
fields: fields{
|
||||
db: mockDB(t).
|
||||
expectBegin().
|
||||
expectSavepoint().
|
||||
expectRenew("locker", "view", 1).
|
||||
expectReleaseSavepoint().
|
||||
expectCommit(),
|
||||
},
|
||||
args: args{lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "renew now rows updated",
|
||||
fields: fields{
|
||||
db: mockDB(t).
|
||||
expectBegin().
|
||||
expectSavepoint().
|
||||
expectRenew("locker", "view", 0).
|
||||
expectRollback(),
|
||||
},
|
||||
args: args{lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := &locker{
|
||||
dbClient: tt.fields.db.db,
|
||||
}
|
||||
if err := l.Renew(tt.args.lockerID, tt.args.viewModel, tt.args.waitTime); (err != nil) != tt.wantErr {
|
||||
t.Errorf("locker.Renew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err := tt.fields.db.mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("not all database expectations met: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/handler"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
|
||||
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type SpoolerConfig struct {
|
||||
BulkLimit uint64
|
||||
FailureCountUntilSkip uint64
|
||||
ConcurrentTasks int
|
||||
Handlers handler.Configs
|
||||
}
|
||||
|
||||
type EventstoreRepos struct {
|
||||
UserEvents *usr_event.UserEventstore
|
||||
}
|
||||
|
||||
func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, sql *sql.DB, eventstoreRepos handler.EventstoreRepos, systemDefaults sd.SystemDefaults) *spooler.Spooler {
|
||||
spoolerConfig := spooler.Config{
|
||||
Eventstore: es,
|
||||
Locker: &locker{dbClient: sql},
|
||||
ConcurrentTasks: c.ConcurrentTasks,
|
||||
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, eventstoreRepos, systemDefaults),
|
||||
}
|
||||
spool := spoolerConfig.New()
|
||||
spool.Start()
|
||||
return spool
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
errTable = "notification.failed_event"
|
||||
)
|
||||
|
||||
func (v *View) saveFailedEvent(failedEvent *view.FailedEvent) error {
|
||||
return view.SaveFailedEvent(v.Db, errTable, failedEvent)
|
||||
}
|
||||
|
||||
func (v *View) latestFailedEvent(viewName string, sequence uint64) (*view.FailedEvent, error) {
|
||||
return view.LatestFailedEvent(v.Db, errTable, viewName, sequence)
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
global_view "github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
notificationTable = "notification.notifications"
|
||||
)
|
||||
|
||||
func (v *View) GetLatestNotificationSequence() (uint64, error) {
|
||||
return v.latestSequence(notificationTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotificationSequence(eventSequence uint64) error {
|
||||
return v.saveCurrentSequence(notificationTable, eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotificationFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(notificationTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotificationFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/user/repository/view"
|
||||
"github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
global_view "github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
notifyUserTable = "notification.notify_users"
|
||||
)
|
||||
|
||||
func (v *View) NotifyUserByID(userID string) (*model.NotifyUser, error) {
|
||||
return view.NotifyUserByID(v.Db, notifyUserTable, userID)
|
||||
}
|
||||
|
||||
func (v *View) PutNotifyUser(user *model.NotifyUser) error {
|
||||
err := view.PutNotifyUser(v.Db, notifyUserTable, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedNotifyUserSequence(user.Sequence)
|
||||
}
|
||||
|
||||
func (v *View) DeleteNotifyUser(userID string, eventSequence uint64) error {
|
||||
err := view.DeleteNotifyUser(v.Db, notifyUserTable, userID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return v.ProcessedNotifyUserSequence(eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotifyUserSequence() (uint64, error) {
|
||||
return v.latestSequence(notifyUserTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotifyUserSequence(eventSequence uint64) error {
|
||||
return v.saveCurrentSequence(notifyUserTable, eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotifyUserFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(notifyUserTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotifyUserFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
sequencesTable = "notification.current_sequences"
|
||||
)
|
||||
|
||||
func (v *View) saveCurrentSequence(viewName string, sequence uint64) error {
|
||||
return view.SaveCurrentSequence(v.Db, sequencesTable, viewName, sequence)
|
||||
}
|
||||
|
||||
func (v *View) latestSequence(viewName string) (uint64, error) {
|
||||
return view.LatestSequence(v.Db, sequencesTable, viewName)
|
||||
}
|
25
internal/notification/repository/eventsourcing/view/view.go
Normal file
25
internal/notification/repository/eventsourcing/view/view.go
Normal file
@@ -0,0 +1,25 @@
|
||||
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()
|
||||
}
|
5
internal/notification/repository/repository.go
Normal file
5
internal/notification/repository/repository.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package repository
|
||||
|
||||
type Repository interface {
|
||||
Health() error
|
||||
}
|
45
internal/notification/templates/template.go
Normal file
45
internal/notification/templates/template.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
const (
|
||||
templateFileName = "template.html"
|
||||
)
|
||||
|
||||
func GetParsedTemplate(contentData interface{}) (string, error) {
|
||||
template, err := ParseTemplateFile("", contentData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ParseTemplateText(template, contentData)
|
||||
}
|
||||
|
||||
func ParseTemplateFile(fileName string, data interface{}) (string, error) {
|
||||
if fileName == "" {
|
||||
fileName = templateFileName
|
||||
}
|
||||
template, err := template.ParseFiles(fileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return parseTemplate(template, data)
|
||||
}
|
||||
|
||||
func ParseTemplateText(text string, data interface{}) (string, error) {
|
||||
template, err := template.New("template").Parse(text)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return parseTemplate(template, data)
|
||||
}
|
||||
|
||||
func parseTemplate(template *template.Template, data interface{}) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := template.Execute(buf, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
11
internal/notification/templates/templateData.go
Normal file
11
internal/notification/templates/templateData.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package templates
|
||||
|
||||
type TemplateData struct {
|
||||
Title string
|
||||
PreHeader string
|
||||
Subject string
|
||||
Greeting string
|
||||
Text string
|
||||
Href string
|
||||
ButtonText string
|
||||
}
|
34
internal/notification/types/email_verification_code.go
Normal file
34
internal/notification/types/email_verification_code.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type EmailVerificationCodeData struct {
|
||||
templates.TemplateData
|
||||
FirstName string
|
||||
LastName string
|
||||
URL string
|
||||
}
|
||||
|
||||
func SendEmailVerificationCode(user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.VerifyEmail, &UrlData{UserID: user.ID, Code: codeString})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emailCodeData := &EmailVerificationCodeData{TemplateData: systemDefaults.Notifications.TemplateData.VerifyEmail, FirstName: user.FirstName, LastName: user.LastName, URL: url}
|
||||
|
||||
template, err := templates.GetParsedTemplate(emailCodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(user, template, systemDefaults.Notifications, true)
|
||||
}
|
39
internal/notification/types/init_code.go
Normal file
39
internal/notification/types/init_code.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type InitCodeEmailData struct {
|
||||
templates.TemplateData
|
||||
FirstName string
|
||||
LastName string
|
||||
URL string
|
||||
}
|
||||
|
||||
type UrlData struct {
|
||||
UserID string
|
||||
Code string
|
||||
}
|
||||
|
||||
func SendUserInitCode(user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.InitCode, &UrlData{UserID: user.ID, Code: codeString})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
initCodeData := &InitCodeEmailData{TemplateData: systemDefaults.Notifications.TemplateData.InitCode, FirstName: user.FirstName, LastName: user.LastName, URL: url}
|
||||
|
||||
template, err := templates.GetParsedTemplate(initCodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(user, template, systemDefaults.Notifications, true)
|
||||
}
|
34
internal/notification/types/password_code.go
Normal file
34
internal/notification/types/password_code.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type PasswordCodeData struct {
|
||||
templates.TemplateData
|
||||
FirstName string
|
||||
LastName string
|
||||
URL string
|
||||
}
|
||||
|
||||
func SendPasswordCode(user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.PasswordReset, &UrlData{UserID: user.ID, Code: codeString})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
passwordCodeData := &PasswordCodeData{TemplateData: systemDefaults.Notifications.TemplateData.PasswordReset, FirstName: user.FirstName, LastName: user.LastName, URL: url}
|
||||
|
||||
template, err := templates.GetParsedTemplate(passwordCodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(user, template, systemDefaults.Notifications, false)
|
||||
}
|
29
internal/notification/types/phone_verification_code.go
Normal file
29
internal/notification/types/phone_verification_code.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type PhoneVerificationCodeData struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Code string
|
||||
UserID string
|
||||
}
|
||||
|
||||
func SendPhoneVerificationCode(user *view_model.NotifyUser, code *es_model.PhoneCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
codeData := &PhoneVerificationCodeData{FirstName: user.FirstName, LastName: user.LastName, UserID: user.ID, Code: codeString}
|
||||
template, err := templates.ParseTemplateText(systemDefaults.Notifications.TemplateData.VerifyPhone.Text, codeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateSms(user, template, systemDefaults.Notifications, true)
|
||||
}
|
41
internal/notification/types/user_email.go
Normal file
41
internal/notification/types/user_email.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
"github.com/caos/zitadel/internal/notification/providers/chat"
|
||||
"github.com/caos/zitadel/internal/notification/providers/email"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
func generateEmail(user *view_model.NotifyUser, content string, config systemdefaults.Notifications, lastEmail bool) error {
|
||||
provider, err := email.InitEmailProvider(config.Providers.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
message := &email.EmailMessage{
|
||||
SenderEmail: config.Providers.Email.From,
|
||||
Recipients: []string{user.VerifiedEmail},
|
||||
Subject: config.TemplateData.InitCode.Subject,
|
||||
Content: content,
|
||||
}
|
||||
if lastEmail {
|
||||
message.Recipients = []string{user.LastEmail}
|
||||
}
|
||||
if provider.CanHandleMessage(message) {
|
||||
if config.DebugMode {
|
||||
return sendDebugEmail(message, config)
|
||||
}
|
||||
return provider.HandleMessage(message)
|
||||
}
|
||||
return caos_errs.ThrowInternalf(nil, "NOTIF-s8ipw", "Could not send init message: userid: %v", user.ID)
|
||||
}
|
||||
|
||||
func sendDebugEmail(message providers.Message, config systemdefaults.Notifications) error {
|
||||
provider, err := chat.InitChatProvider(config.Providers.Chat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return provider.HandleMessage(message)
|
||||
}
|
37
internal/notification/types/user_phone.go
Normal file
37
internal/notification/types/user_phone.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
"github.com/caos/zitadel/internal/notification/providers/chat"
|
||||
"github.com/caos/zitadel/internal/notification/providers/twilio"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
func generateSms(user *view_model.NotifyUser, content string, config systemdefaults.Notifications, lastPhone bool) error {
|
||||
provider := twilio.InitTwilioProvider(config.Providers.Twilio)
|
||||
message := &twilio.TwilioMessage{
|
||||
SenderPhoneNumber: config.Providers.Twilio.From,
|
||||
RecipientPhoneNumber: user.VerifiedPhone,
|
||||
Content: content,
|
||||
}
|
||||
if lastPhone {
|
||||
message.RecipientPhoneNumber = user.LastPhone
|
||||
}
|
||||
if provider.CanHandleMessage(message) {
|
||||
if config.DebugMode {
|
||||
return sendDebugPhone(message, config)
|
||||
}
|
||||
return provider.HandleMessage(message)
|
||||
}
|
||||
return caos_errs.ThrowInternalf(nil, "NOTIF-s8ipw", "Could not send init message: userid: %v", user.ID)
|
||||
}
|
||||
|
||||
func sendDebugPhone(message providers.Message, config systemdefaults.Notifications) error {
|
||||
provider, err := chat.InitChatProvider(config.Providers.Chat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return provider.HandleMessage(message)
|
||||
}
|
53
internal/user/model/notify_user.go
Normal file
53
internal/user/model/notify_user.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NotifyUser struct {
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
UserName string
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
DisplayName string
|
||||
PreferredLanguage string
|
||||
Gender Gender
|
||||
LastEmail string
|
||||
VerifiedEmail string
|
||||
LastPhone string
|
||||
VerifiedPhone string
|
||||
Sequence uint64
|
||||
}
|
||||
|
||||
type NotifyUserSearchRequest struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
SortingColumn NotifyUserSearchKey
|
||||
Asc bool
|
||||
Queries []*NotifyUserSearchQuery
|
||||
}
|
||||
|
||||
type NotifyUserSearchKey int32
|
||||
|
||||
const (
|
||||
NOTIFYUSERSEARCHKEY_UNSPECIFIED UserSearchKey = iota
|
||||
NOTIFYUSERSEARCHKEY_USER_ID
|
||||
)
|
||||
|
||||
type NotifyUserSearchQuery struct {
|
||||
Key NotifyUserSearchKey
|
||||
Method model.SearchMethod
|
||||
Value string
|
||||
}
|
||||
|
||||
type NotifyUserSearchResponse struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
TotalResult uint64
|
||||
Result []*UserView
|
||||
}
|
@@ -1,13 +1,18 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/ttacon/libphonenumber"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
//TODO: How do we get region?
|
||||
defaultRegion = "CH"
|
||||
)
|
||||
|
||||
type Phone struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
@@ -23,27 +28,16 @@ type PhoneCode struct {
|
||||
}
|
||||
|
||||
func (p *Phone) IsValid() bool {
|
||||
return p.PhoneNumber != ""
|
||||
err := p.formatPhone()
|
||||
return p.PhoneNumber != "" && err == nil
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneChangedEvent(event *es_models.Event) error {
|
||||
u.Phone = new(Phone)
|
||||
u.Phone.setData(event)
|
||||
u.IsPhoneVerified = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneVerifiedEvent() error {
|
||||
u.IsPhoneVerified = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Phone) setData(event *es_models.Event) error {
|
||||
p.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, p); err != nil {
|
||||
logging.Log("EVEN-dlo9s").WithError(err).Error("could not unmarshal event data")
|
||||
return err
|
||||
func (p *Phone) formatPhone() error {
|
||||
phoneNr, err := libphonenumber.Parse(p.PhoneNumber, defaultRegion)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-so0wa", "Phonenumber is invalid")
|
||||
}
|
||||
p.PhoneNumber = libphonenumber.Format(phoneNr, libphonenumber.E164)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
106
internal/user/model/phone_test.go
Normal file
106
internal/user/model/phone_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatPhoneNumber(t *testing.T) {
|
||||
type args struct {
|
||||
phone *Phone
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Phone
|
||||
errFunc func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "invalid phone number",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "PhoneNumber",
|
||||
},
|
||||
},
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
{
|
||||
name: "format phone 071...",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "0711234567",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone 0041...",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "0041711234567",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone 071 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "071 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone +4171 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "+4171 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone 004171 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "004171 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format non swiss phone 004371 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "004371 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+43711234567",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.phone.formatPhone()
|
||||
|
||||
if tt.errFunc == nil && tt.result.PhoneNumber != tt.args.phone.PhoneNumber {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.args.phone.PhoneNumber, tt.result.PhoneNumber)
|
||||
}
|
||||
if tt.errFunc != nil && !tt.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -58,7 +58,7 @@ func (u *User) SetEmailAsUsername() {
|
||||
}
|
||||
|
||||
func (u *User) IsValid() bool {
|
||||
return u.Profile != nil && u.FirstName != "" && u.LastName != "" && u.UserName != "" && u.Email != nil && u.EmailAddress != ""
|
||||
return u.Profile != nil && u.FirstName != "" && u.LastName != "" && u.UserName != "" && u.Email != nil && u.Email.IsValid() && u.Phone == nil || (u.Phone != nil && u.Phone.IsValid())
|
||||
}
|
||||
|
||||
func (u *User) IsInitialState() bool {
|
||||
|
@@ -96,7 +96,7 @@ func (es *UserEventstore) UserByID(ctx context.Context, id string) (*usr_model.U
|
||||
func (es *UserEventstore) PrepareCreateUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*model.User, *es_models.Aggregate, error) {
|
||||
user.SetEmailAsUsername()
|
||||
if !user.IsValid() {
|
||||
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "Name is required")
|
||||
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "User is invalid")
|
||||
}
|
||||
//TODO: Check Uniqueness
|
||||
id, err := es.idGenerator.NextID()
|
||||
@@ -303,6 +303,25 @@ func (es *UserEventstore) CreateInitializeUserCodeByID(ctx context.Context, user
|
||||
return model.InitCodeToModel(repoUser.InitCode), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) InitCodeSent(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-0posw", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := UserInitCodeSentAggregate(es.AggregateCreator(), repoUser)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) SkipMfaInit(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
|
||||
@@ -446,6 +465,25 @@ func (es *UserEventstore) RequestSetPassword(ctx context.Context, userID string,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) PasswordCodeSent(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s09ow", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := PasswordCodeSentAggregate(es.AggregateCreator(), repoUser)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
|
||||
@@ -584,6 +622,25 @@ func (es *UserEventstore) CreateEmailVerificationCode(ctx context.Context, userI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) EmailVerificationCodeSent(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-spo0w", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := EmailCodeSentAggregate(es.AggregateCreator(), repoUser)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) PhoneByID(ctx context.Context, userID string) (*usr_model.Phone, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9se", "userID missing")
|
||||
@@ -686,6 +743,25 @@ func (es *UserEventstore) CreatePhoneVerificationCode(ctx context.Context, userI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) PhoneVerificationCodeSent(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0wa", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := PhoneCodeSentAggregate(es.AggregateCreator(), repoUser)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) AddressByID(ctx context.Context, userID string) (*usr_model.Address, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di8ws", "userID missing")
|
||||
|
@@ -161,7 +161,7 @@ func TestCreateUser(t *testing.T) {
|
||||
{
|
||||
name: "with verified phone number",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithInitCodeGen(ctrl, repo_model.User{Profile: &repo_model.Profile{UserName: "EmailAddress", FirstName: "FirstName", LastName: "LastName"}, Email: &repo_model.Email{EmailAddress: "EmailAddress", IsEmailVerified: true}, Phone: &repo_model.Phone{PhoneNumber: "PhoneNumber", IsPhoneVerified: true}}),
|
||||
es: GetMockManipulateUserWithInitCodeGen(ctrl, repo_model.User{Profile: &repo_model.Profile{UserName: "EmailAddress", FirstName: "FirstName", LastName: "LastName"}, Email: &repo_model.Email{EmailAddress: "EmailAddress", IsEmailVerified: true}, Phone: &repo_model.Phone{PhoneNumber: "+41711234567", IsPhoneVerified: true}}),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
user: &model.User{ObjectRoot: es_models.ObjectRoot{Sequence: 1},
|
||||
Profile: &model.Profile{
|
||||
@@ -174,7 +174,7 @@ func TestCreateUser(t *testing.T) {
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
Phone: &model.Phone{
|
||||
PhoneNumber: "UserName",
|
||||
PhoneNumber: "+41711234567",
|
||||
IsPhoneVerified: true,
|
||||
},
|
||||
},
|
||||
@@ -191,7 +191,7 @@ func TestCreateUser(t *testing.T) {
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
Phone: &model.Phone{
|
||||
PhoneNumber: "UserName",
|
||||
PhoneNumber: "+41711234567",
|
||||
IsPhoneVerified: true,
|
||||
},
|
||||
},
|
||||
@@ -824,6 +824,67 @@ func TestCreateInitCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCodeSent(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "sent init",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.InitCodeSent(tt.args.ctx, tt.args.existing.AggregateID)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("rshould not get err")
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkipMfaInit(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
@@ -1468,6 +1529,67 @@ func TestRequestSetPassword(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordCodeSent(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "sent password code",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.PasswordCodeSent(tt.args.ctx, tt.args.existing.AggregateID)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("rshould not get err")
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileByID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
@@ -1907,6 +2029,67 @@ func TestCreateEmailVerificationCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailVerificationCodeSent(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "sent email verify code",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.EmailVerificationCodeSent(tt.args.ctx, tt.args.existing.AggregateID)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("rshould not get err")
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhoneByID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
@@ -1995,10 +2178,10 @@ func TestChangePhone(t *testing.T) {
|
||||
args: args{
|
||||
es: GetMockManipulateUserFull(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "PhoneNumberChanged", IsPhoneVerified: true},
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "0711234567", IsPhoneVerified: true},
|
||||
},
|
||||
res: res{
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "PhoneNumberChanged", IsPhoneVerified: true},
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "+41711234567", IsPhoneVerified: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2006,10 +2189,10 @@ func TestChangePhone(t *testing.T) {
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithPhoneCodeGen(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, Profile: &repo_model.Profile{UserName: "UserName"}, Phone: &repo_model.Phone{PhoneNumber: "PhoneNumber"}}),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "PhoneNumberChanged", IsPhoneVerified: false},
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "+41711234567", IsPhoneVerified: false},
|
||||
},
|
||||
res: res{
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "PhoneNumberChanged", IsPhoneVerified: false},
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "+41711234567", IsPhoneVerified: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2028,7 +2211,7 @@ func TestChangePhone(t *testing.T) {
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "PhoneNumberChanged", IsPhoneVerified: true},
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "+41711234567", IsPhoneVerified: true},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
@@ -2212,6 +2395,67 @@ func TestCreatePhoneVerificationCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhoneVerificationCodeSent(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "sent phone verification code",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.PhoneVerificationCodeSent(tt.args.ctx, tt.args.existing.AggregateID)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("rshould not get err")
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressByID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
|
@@ -76,7 +76,7 @@ func (u *User) appendUserEmailChangedEvent(event *es_models.Event) error {
|
||||
|
||||
func (u *User) appendUserEmailCodeAddedEvent(event *es_models.Event) error {
|
||||
u.EmailCode = new(EmailCode)
|
||||
return u.EmailCode.setData(event)
|
||||
return u.EmailCode.SetData(event)
|
||||
}
|
||||
|
||||
func (u *User) appendUserEmailVerifiedEvent() {
|
||||
@@ -92,7 +92,7 @@ func (a *Email) setData(event *es_models.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *EmailCode) setData(event *es_models.Event) error {
|
||||
func (a *EmailCode) SetData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-lo9s").WithError(err).Error("could not unmarshal event data")
|
||||
|
@@ -62,7 +62,7 @@ func (u *User) appendUserPasswordChangedEvent(event *es_models.Event) error {
|
||||
|
||||
func (u *User) appendPasswordSetRequestedEvent(event *es_models.Event) error {
|
||||
u.PasswordCode = new(PasswordCode)
|
||||
return u.PasswordCode.setData(event)
|
||||
return u.PasswordCode.SetData(event)
|
||||
}
|
||||
|
||||
func (pw *Password) setData(event *es_models.Event) error {
|
||||
@@ -74,7 +74,7 @@ func (pw *Password) setData(event *es_models.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *PasswordCode) setData(event *es_models.Event) error {
|
||||
func (a *PasswordCode) SetData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-lo0y2").WithError(err).Error("could not unmarshal event data")
|
||||
|
@@ -74,7 +74,7 @@ func (u *User) appendUserPhoneChangedEvent(event *es_models.Event) error {
|
||||
|
||||
func (u *User) appendUserPhoneCodeAddedEvent(event *es_models.Event) error {
|
||||
u.PhoneCode = new(PhoneCode)
|
||||
return u.PhoneCode.setData(event)
|
||||
return u.PhoneCode.SetData(event)
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneVerifiedEvent() {
|
||||
@@ -90,7 +90,7 @@ func (p *Phone) setData(event *es_models.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *PhoneCode) setData(event *es_models.Event) error {
|
||||
func (a *PhoneCode) SetData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-sk8ws").WithError(err).Error("could not unmarshal event data")
|
||||
|
@@ -228,7 +228,7 @@ func (u *User) appendUnlockedEvent() {
|
||||
|
||||
func (u *User) appendInitUsercodeCreatedEvent(event *es_models.Event) error {
|
||||
initCode := new(InitUserCode)
|
||||
err := initCode.setData(event)
|
||||
err := initCode.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -237,7 +237,7 @@ func (u *User) appendInitUsercodeCreatedEvent(event *es_models.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *InitUserCode) setData(event *es_models.Event) error {
|
||||
func (c *InitUserCode) SetData(event *es_models.Event) error {
|
||||
c.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, c); err != nil {
|
||||
logging.Log("EVEN-7duwe").WithError(err).Error("could not unmarshal event data")
|
||||
|
@@ -68,12 +68,6 @@ func UserCreateAggregate(ctx context.Context, aggCreator *es_models.AggregateCre
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user.Password != nil {
|
||||
agg, err = agg.AppendEvent(model.UserPasswordCodeAdded, user.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if initCode != nil {
|
||||
agg, err = agg.AppendEvent(model.InitializedUserCodeAdded, initCode)
|
||||
if err != nil {
|
||||
@@ -145,6 +139,16 @@ func UserInitCodeAggregate(aggCreator *es_models.AggregateCreator, existing *mod
|
||||
}
|
||||
}
|
||||
|
||||
func UserInitCodeSentAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.InitializedUserCodeSent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func SkipMfaAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
@@ -200,6 +204,16 @@ func RequestSetPassword(aggCreator *es_models.AggregateCreator, existing *model.
|
||||
}
|
||||
}
|
||||
|
||||
func PasswordCodeSentAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserPasswordCodeSent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func ProfileChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, profile *model.Profile) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if profile == nil {
|
||||
@@ -210,6 +224,9 @@ func ProfileChangeAggregate(aggCreator *es_models.AggregateCreator, existing *mo
|
||||
return nil, err
|
||||
}
|
||||
changes := existing.Profile.Changes(profile)
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-0spow", "no changes found")
|
||||
}
|
||||
return agg.AppendEvent(model.UserProfileChanged, changes)
|
||||
}
|
||||
}
|
||||
@@ -227,6 +244,9 @@ func EmailChangeAggregate(aggCreator *es_models.AggregateCreator, existing *mode
|
||||
return nil, err
|
||||
}
|
||||
changes := existing.Email.Changes(email)
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-s90pw", "no changes found")
|
||||
}
|
||||
agg, err = agg.AppendEvent(model.UserEmailChanged, changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -266,6 +286,16 @@ func EmailVerificationCodeAggregate(aggCreator *es_models.AggregateCreator, exis
|
||||
}
|
||||
}
|
||||
|
||||
func EmailCodeSentAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserEmailCodeSent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, phone *model.Phone, code *model.PhoneCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if phone == nil {
|
||||
@@ -282,6 +312,9 @@ func PhoneChangeAggregate(aggCreator *es_models.AggregateCreator, existing *mode
|
||||
existing.Phone = new(model.Phone)
|
||||
}
|
||||
changes := existing.Phone.Changes(phone)
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-sp0oc", "no changes found")
|
||||
}
|
||||
agg, err = agg.AppendEvent(model.UserPhoneChanged, changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -318,6 +351,16 @@ func PhoneVerificationCodeAggregate(aggCreator *es_models.AggregateCreator, exis
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneCodeSentAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserPhoneCodeSent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func AddressChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, address *model.Address) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if address == nil {
|
||||
@@ -331,6 +374,9 @@ func AddressChangeAggregate(aggCreator *es_models.AggregateCreator, existing *mo
|
||||
existing.Address = new(model.Address)
|
||||
}
|
||||
changes := existing.Address.Changes(address)
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-2tszw", "no changes found")
|
||||
}
|
||||
return agg.AppendEvent(model.UserAddressChanged, changes)
|
||||
}
|
||||
}
|
||||
|
@@ -650,6 +650,54 @@ func TestUserInitCodeAggregate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCodeSentAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
aggCreator *models.AggregateCreator
|
||||
}
|
||||
type res struct {
|
||||
eventLen int
|
||||
eventTypes []models.EventType
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "user init code sent aggregate ok",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
eventLen: 1,
|
||||
eventTypes: []models.EventType{model.InitializedUserCodeSent},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
agg, err := UserInitCodeSentAggregate(tt.args.aggCreator, tt.args.existing)(tt.args.ctx)
|
||||
|
||||
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
|
||||
}
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkipMfaAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -824,6 +872,54 @@ func TestRequestSetPasswordAggregate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordCodeSentAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
aggCreator *models.AggregateCreator
|
||||
}
|
||||
type res struct {
|
||||
eventLen int
|
||||
eventTypes []models.EventType
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "user password code sent aggregate ok",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
eventLen: 1,
|
||||
eventTypes: []models.EventType{model.UserPasswordCodeSent},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
agg, err := PasswordCodeSentAggregate(tt.args.aggCreator, tt.args.existing)(tt.args.ctx)
|
||||
|
||||
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
|
||||
}
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeProfileAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -846,9 +942,9 @@ func TestChangeProfileAggregate(t *testing.T) {
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
|
||||
Profile: &model.Profile{UserName: "UserName"},
|
||||
Profile: &model.Profile{FirstName: "FirstName"},
|
||||
},
|
||||
profile: &model.Profile{FirstName: ""},
|
||||
profile: &model.Profile{FirstName: "FirstNameChanged"},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
@@ -1114,6 +1210,55 @@ func TestCreateEmailCodeAggregate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailCodeSentAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
aggCreator *models.AggregateCreator
|
||||
}
|
||||
type res struct {
|
||||
eventLen int
|
||||
eventTypes []models.EventType
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "user email code sent aggregate ok",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
eventLen: 1,
|
||||
eventTypes: []models.EventType{model.UserEmailCodeSent},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
agg, err := EmailCodeSentAggregate(tt.args.aggCreator, tt.args.existing)(tt.args.ctx)
|
||||
|
||||
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
|
||||
}
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangePhoneAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -1137,9 +1282,9 @@ func TestChangePhoneAggregate(t *testing.T) {
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
|
||||
Phone: &model.Phone{PhoneNumber: "PhoneNumber"},
|
||||
Phone: &model.Phone{PhoneNumber: "+41791234567"},
|
||||
},
|
||||
phone: &model.Phone{PhoneNumber: "Changed", IsPhoneVerified: true},
|
||||
phone: &model.Phone{PhoneNumber: "+41799876543", IsPhoneVerified: true},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
@@ -1346,6 +1491,54 @@ func TestCreatePhoneCodeAggregate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhoneCodeSentAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
aggCreator *models.AggregateCreator
|
||||
}
|
||||
type res struct {
|
||||
eventLen int
|
||||
eventTypes []models.EventType
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "user phone code sent aggregate ok",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
eventLen: 1,
|
||||
eventTypes: []models.EventType{model.UserPhoneCodeSent},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
agg, err := PhoneCodeSentAggregate(tt.args.aggCreator, tt.args.existing)(tt.args.ctx)
|
||||
|
||||
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
|
||||
}
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeAddressAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
|
112
internal/user/repository/view/model/notify_user.go
Normal file
112
internal/user/repository/view/model/notify_user.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
NotifyUserKeyUserID = "id"
|
||||
)
|
||||
|
||||
type NotifyUser struct {
|
||||
ID string `json:"-" gorm:"column:id;primary_key"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
UserName string `json:"userName" gorm:"column:user_name"`
|
||||
FirstName string `json:"firstName" gorm:"column:first_name"`
|
||||
LastName string `json:"lastName" gorm:"column:last_name"`
|
||||
NickName string `json:"nickName" gorm:"column:nick_name"`
|
||||
DisplayName string `json:"displayName" gorm:"column:display_name"`
|
||||
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
|
||||
Gender int32 `json:"gender" gorm:"column:gender"`
|
||||
LastEmail string `json:"email" gorm:"column:last_email"`
|
||||
VerifiedEmail string `json:"-" gorm:"column:verified_email"`
|
||||
LastPhone string `json:"phone" gorm:"column:last_phone"`
|
||||
VerifiedPhone string `json:"-" gorm:"column:verified_phone"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
}
|
||||
|
||||
func NotifyUserFromModel(user *model.NotifyUser) *NotifyUser {
|
||||
return &NotifyUser{
|
||||
ID: user.ID,
|
||||
ChangeDate: user.ChangeDate,
|
||||
CreationDate: user.CreationDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
UserName: user.UserName,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
NickName: user.NickName,
|
||||
DisplayName: user.DisplayName,
|
||||
PreferredLanguage: user.PreferredLanguage,
|
||||
Gender: int32(user.Gender),
|
||||
LastEmail: user.LastEmail,
|
||||
VerifiedEmail: user.VerifiedEmail,
|
||||
LastPhone: user.LastPhone,
|
||||
VerifiedPhone: user.VerifiedPhone,
|
||||
Sequence: user.Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
func NotifyUserToModel(user *NotifyUser) *model.NotifyUser {
|
||||
return &model.NotifyUser{
|
||||
ID: user.ID,
|
||||
ChangeDate: user.ChangeDate,
|
||||
CreationDate: user.CreationDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
UserName: user.UserName,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
NickName: user.NickName,
|
||||
DisplayName: user.DisplayName,
|
||||
PreferredLanguage: user.PreferredLanguage,
|
||||
Gender: model.Gender(user.Gender),
|
||||
LastEmail: user.LastEmail,
|
||||
VerifiedEmail: user.VerifiedEmail,
|
||||
LastPhone: user.LastPhone,
|
||||
VerifiedPhone: user.VerifiedPhone,
|
||||
Sequence: user.Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *NotifyUser) AppendEvent(event *models.Event) (err error) {
|
||||
u.ChangeDate = event.CreationDate
|
||||
u.Sequence = event.Sequence
|
||||
switch event.Type {
|
||||
case es_model.UserAdded,
|
||||
es_model.UserRegistered:
|
||||
u.CreationDate = event.CreationDate
|
||||
u.setRootData(event)
|
||||
err = u.setData(event)
|
||||
case es_model.UserProfileChanged:
|
||||
err = u.setData(event)
|
||||
case es_model.UserEmailChanged:
|
||||
err = u.setData(event)
|
||||
case es_model.UserEmailVerified:
|
||||
u.VerifiedEmail = u.LastEmail
|
||||
case es_model.UserPhoneChanged:
|
||||
err = u.setData(event)
|
||||
case es_model.UserPhoneVerified:
|
||||
u.VerifiedPhone = u.LastPhone
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *NotifyUser) setRootData(event *models.Event) {
|
||||
u.ID = event.AggregateID
|
||||
u.ResourceOwner = event.ResourceOwner
|
||||
}
|
||||
|
||||
func (u *NotifyUser) setData(event *models.Event) error {
|
||||
if err := json.Unmarshal(event.Data, u); err != nil {
|
||||
logging.Log("EVEN-lso9e").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(nil, "MODEL-8iows", "could not unmarshal data")
|
||||
}
|
||||
return nil
|
||||
}
|
113
internal/user/repository/view/model/notify_user_test.go
Normal file
113
internal/user/repository/view/model/notify_user_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNotifyUserAppendEvent(t *testing.T) {
|
||||
type args struct {
|
||||
event *es_models.Event
|
||||
user *NotifyUser
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *NotifyUser
|
||||
}{
|
||||
{
|
||||
name: "append added user event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserAdded, ResourceOwner: "OrgID", Data: mockUserData(getFullUser(nil))},
|
||||
user: &NotifyUser{},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append change user profile event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserProfileChanged, ResourceOwner: "OrgID", Data: mockProfileData(&es_model.Profile{FirstName: "FirstNameChanged"})},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstNameChanged", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append change user email event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserEmailChanged, ResourceOwner: "OrgID", Data: mockEmailData(&es_model.Email{EmailAddress: "EmailChanged"})},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "EmailChanged", LastPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append change user email event, existing email",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserEmailChanged, ResourceOwner: "OrgID", Data: mockEmailData(&es_model.Email{EmailAddress: "EmailChanged"})},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", VerifiedEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "EmailChanged", VerifiedEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append verify user email event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserEmailVerified, ResourceOwner: "OrgID"},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", VerifiedEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append change user phone event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserPhoneChanged, ResourceOwner: "OrgID", Data: mockPhoneData(&es_model.Phone{PhoneNumber: "PhoneChanged"})},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "PhoneChanged"},
|
||||
},
|
||||
{
|
||||
name: "append change user phone event, existing phone",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserPhoneChanged, ResourceOwner: "OrgID", Data: mockPhoneData(&es_model.Phone{PhoneNumber: "PhoneChanged"})},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone", VerifiedPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "PhoneChanged", VerifiedPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append verify user phone event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserPhoneVerified, ResourceOwner: "OrgID"},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone", VerifiedPhone: "Phone"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.user.AppendEvent(tt.args.event)
|
||||
if tt.args.user.ID != tt.result.ID {
|
||||
t.Errorf("got wrong result ID: expected: %v, actual: %v ", tt.result.ID, tt.args.user.ID)
|
||||
}
|
||||
if tt.args.user.FirstName != tt.result.FirstName {
|
||||
t.Errorf("got wrong result FirstName: expected: %v, actual: %v ", tt.result.FirstName, tt.args.user.FirstName)
|
||||
}
|
||||
if tt.args.user.LastName != tt.result.LastName {
|
||||
t.Errorf("got wrong result FirstName: expected: %v, actual: %v ", tt.result.FirstName, tt.args.user.FirstName)
|
||||
}
|
||||
if tt.args.user.ResourceOwner != tt.result.ResourceOwner {
|
||||
t.Errorf("got wrong result ResourceOwner: expected: %v, actual: %v ", tt.result.ResourceOwner, tt.args.user.ResourceOwner)
|
||||
}
|
||||
if tt.args.user.LastEmail != tt.result.LastEmail {
|
||||
t.Errorf("got wrong result LastEmail: expected: %v, actual: %v ", tt.result.LastEmail, tt.args.user.LastEmail)
|
||||
}
|
||||
if tt.args.user.VerifiedEmail != tt.result.VerifiedEmail {
|
||||
t.Errorf("got wrong result VerifiedEmail: expected: %v, actual: %v ", tt.result.VerifiedEmail, tt.args.user.VerifiedEmail)
|
||||
}
|
||||
if tt.args.user.LastPhone != tt.result.LastPhone {
|
||||
t.Errorf("got wrong result LastPhone: expected: %v, actual: %v ", tt.result.LastPhone, tt.args.user.LastPhone)
|
||||
}
|
||||
if tt.args.user.VerifiedPhone != tt.result.VerifiedPhone {
|
||||
t.Errorf("got wrong result VerifiedPhone: expected: %v, actual: %v ", tt.result.VerifiedPhone, tt.args.user.VerifiedPhone)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
25
internal/user/repository/view/notify_user.go
Normal file
25
internal/user/repository/view/notify_user.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
"github.com/caos/zitadel/internal/view"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
func NotifyUserByID(db *gorm.DB, table, userID string) (*model.NotifyUser, error) {
|
||||
user := new(model.NotifyUser)
|
||||
query := view.PrepareGetByKey(table, model.UserSearchKey(usr_model.NOTIFYUSERSEARCHKEY_USER_ID), userID)
|
||||
err := query(db, user)
|
||||
return user, err
|
||||
}
|
||||
|
||||
func PutNotifyUser(db *gorm.DB, table string, project *model.NotifyUser) error {
|
||||
save := view.PrepareSave(table)
|
||||
return save(db, project)
|
||||
}
|
||||
|
||||
func DeleteNotifyUser(db *gorm.DB, table, userID string) error {
|
||||
delete := view.PrepareDeleteByKey(table, model.UserSearchKey(usr_model.NOTIFYUSERSEARCHKEY_USER_ID), userID)
|
||||
return delete(db)
|
||||
}
|
@@ -76,7 +76,7 @@ func LatestFailedEvent(db *gorm.DB, table, viewName string, sequence uint64) (*F
|
||||
query := PrepareGetByQuery(table, queries...)
|
||||
err := query(db, failedEvent)
|
||||
|
||||
if err == nil {
|
||||
if err == nil && failedEvent.ViewName != "" {
|
||||
return failedEvent, nil
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user