mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 16:07:24 +00:00
feat: add stdout and filesystem notification channels (#2925)
* feat: add filesystem and stdout notification channels * configure through env vars * compile * feat: add compact option for debug notification channels * fix channel mock generation * avoid sensitive information in error message Co-authored-by: Livio Amstutz <livio.a@gmail.com> * add review improvements Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
parent
2bbbc3551a
commit
aa2a1848da
1
.gitignore
vendored
1
.gitignore
vendored
@ -58,3 +58,4 @@ openapi/**/*.json
|
||||
# local
|
||||
build/local/cloud.env
|
||||
migrations/cockroach/migrate_cloud.go
|
||||
.notifications
|
||||
|
@ -142,6 +142,7 @@ services:
|
||||
ENV: dev
|
||||
volumes:
|
||||
- ../../.keys:/go/src/github.com/caos/zitadel/.keys
|
||||
- ../../.notifications:/go/src/github.com/caos/zitadel/.notifications
|
||||
env_file:
|
||||
- ./local.env
|
||||
environment:
|
||||
|
@ -28,14 +28,12 @@ DEBUG_MODE=true
|
||||
CAOS_OIDC_DEV=true
|
||||
#sets the cookies insecure in login (never use this in production!)
|
||||
ZITADEL_CSRF_DEV=true
|
||||
|
||||
#currently needed
|
||||
TWILIO_SENDER_NAME=ZITADEL developer
|
||||
SMTP_HOST=smtp.gmail.com:465
|
||||
SMTP_USER=zitadel@caos.ch
|
||||
EMAIL_SENDER_ADDRESS=noreply@caos.ch
|
||||
EMAIL_SENDER_NAME=CAOS AG
|
||||
SMTP_TLS=true
|
||||
LOG_NOTIFICATIONS_ENABLED=true
|
||||
LOG_NOTIFICATIONS_COMPACT=true
|
||||
FS_NOTIFICATIONS_ENABLED=true
|
||||
FS_NOTIFICATIONS_PATH=./.notifications
|
||||
FS_NOTIFICATIONS_COMPACT=false
|
||||
CHAT_ENABLED=false
|
||||
|
||||
#configuration for api/browser calls
|
||||
ZITADEL_DEFAULT_DOMAIN=localhost
|
||||
|
@ -83,9 +83,6 @@ SystemDefaults:
|
||||
DomainClaimed: '$ZITADEL_ACCOUNTS/login'
|
||||
PasswordlessRegistration: '$ZITADEL_ACCOUNTS/login/passwordless/init'
|
||||
Providers:
|
||||
Chat:
|
||||
Url: $CHAT_URL
|
||||
SplitCount: 4000
|
||||
Email:
|
||||
SMTP:
|
||||
Host: $SMTP_HOST
|
||||
@ -98,6 +95,18 @@ SystemDefaults:
|
||||
SID: $TWILIO_SERVICE_SID
|
||||
Token: $TWILIO_TOKEN
|
||||
From: $TWILIO_SENDER_NAME
|
||||
FileSystem:
|
||||
Enabled: $FS_NOTIFICATIONS_ENABLED
|
||||
Path: $FS_NOTIFICATIONS_PATH
|
||||
Compact: $FS_NOTIFICATIONS_COMPACT
|
||||
Log:
|
||||
Enabled: $LOG_NOTIFICATIONS_ENABLED
|
||||
Compact: $LOG_NOTIFICATIONS_COMPACT
|
||||
Chat:
|
||||
Enabled: $CHAT_ENABLED
|
||||
Url: $CHAT_URL
|
||||
Compact: $CHAT_COMPACT
|
||||
SplitCount: 4000
|
||||
TemplateData:
|
||||
InitCode:
|
||||
Title: 'InitCode.Title'
|
||||
|
1
go.mod
1
go.mod
@ -149,6 +149,7 @@ require (
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.11 // indirect
|
||||
github.com/k3a/html2text v1.0.8 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/go-types v0.0.0-20210723172823-2deba1f80ba7 // indirect
|
||||
github.com/kevinburke/rest v0.0.0-20210506044642-5611499aa33c // indirect
|
||||
|
2
go.sum
2
go.sum
@ -629,6 +629,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/k3a/html2text v1.0.8 h1:rVanLhKilpnJUJs/CNKWzMC4YaQINGxK0rSG8ssmnV0=
|
||||
github.com/k3a/html2text v1.0.8/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
|
||||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
|
||||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
|
@ -1,13 +1,15 @@
|
||||
package systemdefaults
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/notification/channels/log"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"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/channels/chat"
|
||||
"github.com/caos/zitadel/internal/notification/channels/fs"
|
||||
"github.com/caos/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/caos/zitadel/internal/notification/channels/twilio"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
)
|
||||
|
||||
@ -69,7 +71,7 @@ type DomainVerification struct {
|
||||
type Notifications struct {
|
||||
DebugMode bool
|
||||
Endpoints Endpoints
|
||||
Providers Providers
|
||||
Providers Channels
|
||||
TemplateData TemplateData
|
||||
}
|
||||
|
||||
@ -81,10 +83,12 @@ type Endpoints struct {
|
||||
PasswordlessRegistration string
|
||||
}
|
||||
|
||||
type Providers struct {
|
||||
type Channels struct {
|
||||
Chat chat.ChatConfig
|
||||
Email email.EmailConfig
|
||||
Email smtp.EmailConfig
|
||||
Twilio twilio.TwilioConfig
|
||||
FileSystem fs.FSConfig
|
||||
Log log.LogConfig
|
||||
}
|
||||
|
||||
type TemplateData struct {
|
||||
|
17
internal/notification/channels/channel.go
Normal file
17
internal/notification/channels/channel.go
Normal file
@ -0,0 +1,17 @@
|
||||
package channels
|
||||
|
||||
type Message interface {
|
||||
GetContent() string
|
||||
}
|
||||
|
||||
type NotificationChannel interface {
|
||||
HandleMessage(message Message) error
|
||||
}
|
||||
|
||||
var _ NotificationChannel = (HandleMessageFunc)(nil)
|
||||
|
||||
type HandleMessageFunc func(message Message) error
|
||||
|
||||
func (h HandleMessageFunc) HandleMessage(message Message) error {
|
||||
return h(message)
|
||||
}
|
@ -3,57 +3,48 @@ package chat
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/k3a/html2text"
|
||||
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/channels"
|
||||
)
|
||||
|
||||
type Chat struct {
|
||||
URL *url.URL
|
||||
SplitCount int
|
||||
}
|
||||
func InitChatChannel(config ChatConfig) (channels.NotificationChannel, error) {
|
||||
|
||||
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
|
||||
}
|
||||
logging.Log("NOTIF-kSvPp").Debug("successfully initialized chat email and sms channel")
|
||||
|
||||
func (chat *Chat) HandleMessage(message providers.Message) error {
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
contentText := message.GetContent()
|
||||
for _, splittedMsg := range splitMessage(contentText, chat.SplitCount) {
|
||||
chatMsg := &ChatMessage{Text: splittedMsg}
|
||||
if err := chat.SendMessage(chatMsg); err != nil {
|
||||
if config.Compact {
|
||||
contentText = html2text.HTML2Text(contentText)
|
||||
}
|
||||
for _, splittedMsg := range splitMessage(contentText, config.SplitCount) {
|
||||
if err := sendMessage(splittedMsg, url); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}), 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)
|
||||
func sendMessage(message string, chatUrl *url.URL) error {
|
||||
req, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "PROVI-s8uie", "Could not unmarshal content")
|
||||
}
|
||||
|
||||
response, err := http.Post(chat.URL.String(), "application/json; charset=UTF-8", bytes.NewReader(req))
|
||||
response, err := http.Post(chatUrl.String(), "application/json; charset=UTF-8", bytes.NewReader(req))
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "PROVI-si93s", "unable to send message")
|
||||
}
|
9
internal/notification/channels/chat/config.go
Normal file
9
internal/notification/channels/chat/config.go
Normal file
@ -0,0 +1,9 @@
|
||||
package chat
|
||||
|
||||
type ChatConfig struct {
|
||||
// Defaults to true if DebugMode is set to true
|
||||
Enabled *bool
|
||||
Url string
|
||||
SplitCount int
|
||||
Compact bool
|
||||
}
|
51
internal/notification/channels/fs/channel.go
Normal file
51
internal/notification/channels/fs/channel.go
Normal file
@ -0,0 +1,51 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
caos_errors "github.com/caos/zitadel/internal/errors"
|
||||
|
||||
"github.com/k3a/html2text"
|
||||
|
||||
"github.com/caos/zitadel/internal/notification/channels"
|
||||
"github.com/caos/zitadel/internal/notification/messages"
|
||||
)
|
||||
|
||||
func InitFSChannel(config FSConfig) (channels.NotificationChannel, error) {
|
||||
|
||||
if err := os.MkdirAll(config.Path, os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logging.Log("NOTIF-kSvPp").Debug("successfully initialized filesystem email and sms channel")
|
||||
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
|
||||
fileName := fmt.Sprintf("%d_", time.Now().Unix())
|
||||
content := message.GetContent()
|
||||
switch msg := message.(type) {
|
||||
case *messages.Email:
|
||||
recipients := make([]string, len(msg.Recipients))
|
||||
copy(recipients, msg.Recipients)
|
||||
sort.Strings(recipients)
|
||||
fileName = fileName + "mail_to_" + strings.Join(recipients, "_") + ".html"
|
||||
if config.Compact {
|
||||
content = html2text.HTML2Text(content)
|
||||
}
|
||||
case *messages.SMS:
|
||||
fileName = fileName + "sms_to_" + msg.RecipientPhoneNumber + ".txt"
|
||||
default:
|
||||
return caos_errors.ThrowUnimplementedf(nil, "NOTIF-6f9a1", "filesystem provider doesn't support message type %T", message)
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(filepath.Join(config.Path, fileName), []byte(content), 0666)
|
||||
}), nil
|
||||
}
|
7
internal/notification/channels/fs/config.go
Normal file
7
internal/notification/channels/fs/config.go
Normal file
@ -0,0 +1,7 @@
|
||||
package fs
|
||||
|
||||
type FSConfig struct {
|
||||
Enabled bool
|
||||
Path string
|
||||
Compact bool
|
||||
}
|
4
internal/notification/channels/gen_mock.go
Normal file
4
internal/notification/channels/gen_mock.go
Normal file
@ -0,0 +1,4 @@
|
||||
package channels
|
||||
|
||||
//go:generate mockgen -package mock -destination ./mock/channel.mock.go github.com/caos/zitadel/internal/notification/channels NotificationChannel
|
||||
//go:generate mockgen -package mock -destination ./mock/message.mock.go github.com/caos/zitadel/internal/notification/channels Message
|
29
internal/notification/channels/log/channel.go
Normal file
29
internal/notification/channels/log/channel.go
Normal file
@ -0,0 +1,29 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k3a/html2text"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/notification/channels"
|
||||
)
|
||||
|
||||
func InitStdoutChannel(config LogConfig) channels.NotificationChannel {
|
||||
|
||||
logging.Log("NOTIF-D0164").Debug("successfully initialized stdout email and sms channel")
|
||||
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
|
||||
content := message.GetContent()
|
||||
if config.Compact {
|
||||
content = html2text.HTML2Text(content)
|
||||
}
|
||||
|
||||
logging.Log("NOTIF-c73ba").WithFields(map[string]interface{}{
|
||||
"type": fmt.Sprintf("%T", message),
|
||||
"content": content,
|
||||
}).Info("handling notification message")
|
||||
return nil
|
||||
})
|
||||
}
|
6
internal/notification/channels/log/config.go
Normal file
6
internal/notification/channels/log/config.go
Normal file
@ -0,0 +1,6 @@
|
||||
package log
|
||||
|
||||
type LogConfig struct {
|
||||
Enabled bool
|
||||
Compact bool
|
||||
}
|
48
internal/notification/channels/mock/channel.mock.go
Normal file
48
internal/notification/channels/mock/channel.mock.go
Normal file
@ -0,0 +1,48 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/caos/zitadel/internal/notification/channels (interfaces: NotificationChannel)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
channels "github.com/caos/zitadel/internal/notification/channels"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockNotificationChannel is a mock of NotificationChannel interface
|
||||
type MockNotificationChannel struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockNotificationChannelMockRecorder
|
||||
}
|
||||
|
||||
// MockNotificationChannelMockRecorder is the mock recorder for MockNotificationChannel
|
||||
type MockNotificationChannelMockRecorder struct {
|
||||
mock *MockNotificationChannel
|
||||
}
|
||||
|
||||
// NewMockNotificationChannel creates a new mock instance
|
||||
func NewMockNotificationChannel(ctrl *gomock.Controller) *MockNotificationChannel {
|
||||
mock := &MockNotificationChannel{ctrl: ctrl}
|
||||
mock.recorder = &MockNotificationChannelMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockNotificationChannel) EXPECT() *MockNotificationChannelMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// HandleMessage mocks base method
|
||||
func (m *MockNotificationChannel) HandleMessage(arg0 channels.Message) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HandleMessage", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HandleMessage indicates an expected call of HandleMessage
|
||||
func (mr *MockNotificationChannelMockRecorder) HandleMessage(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleMessage", reflect.TypeOf((*MockNotificationChannel)(nil).HandleMessage), arg0)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/caos/zitadel/internal/notification/providers (interfaces: Message)
|
||||
// Source: github.com/caos/zitadel/internal/notification/channels (interfaces: Message)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
@ -1,44 +1,48 @@
|
||||
package email
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/caos/zitadel/internal/notification/messages"
|
||||
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
"github.com/caos/zitadel/internal/notification/channels"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _ channels.NotificationChannel = (*Email)(nil)
|
||||
|
||||
type Email struct {
|
||||
smtpClient *smtp.Client
|
||||
}
|
||||
|
||||
func InitEmailProvider(config EmailConfig) (*Email, error) {
|
||||
func InitSMTPChannel(config EmailConfig) (*Email, error) {
|
||||
client, err := config.SMTP.connectToSMTP(config.Tls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logging.Log("NOTIF-4n4Ih").Debug("successfully initialized smtp email channel")
|
||||
|
||||
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 {
|
||||
func (email *Email) HandleMessage(message channels.Message) error {
|
||||
defer email.smtpClient.Close()
|
||||
emailMsg, ok := message.(*EmailMessage)
|
||||
emailMsg, ok := message.(*messages.Email)
|
||||
if !ok {
|
||||
return caos_errs.ThrowInternal(nil, "EMAIL-s8JLs", "message is not EmailMessage")
|
||||
}
|
||||
|
||||
if emailMsg.Content == "" || emailMsg.Subject == "" || len(emailMsg.Recipients) == 0 {
|
||||
return caos_errs.ThrowInternalf(nil, "EMAIL-zGemZ", "subject, recipients and content must be set but got subject %s, recipients length %d and content length %d", emailMsg.Subject, len(emailMsg.Recipients), len(emailMsg.Content))
|
||||
}
|
||||
|
||||
// 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)
|
@ -1,4 +1,4 @@
|
||||
package email
|
||||
package smtp
|
||||
|
||||
type EmailConfig struct {
|
||||
SMTP SMTP
|
28
internal/notification/channels/twilio/channel.go
Normal file
28
internal/notification/channels/twilio/channel.go
Normal file
@ -0,0 +1,28 @@
|
||||
package twilio
|
||||
|
||||
import (
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/channels"
|
||||
"github.com/caos/zitadel/internal/notification/messages"
|
||||
twilio "github.com/kevinburke/twilio-go"
|
||||
)
|
||||
|
||||
func InitTwilioChannel(config TwilioConfig) channels.NotificationChannel {
|
||||
client := twilio.NewClient(config.SID, config.Token, nil)
|
||||
|
||||
logging.Log("NOTIF-KaxDZ").Debug("successfully initialized twilio sms channel")
|
||||
|
||||
return channels.HandleMessageFunc(func(message channels.Message) error {
|
||||
twilioMsg, ok := message.(*messages.SMS)
|
||||
if !ok {
|
||||
return caos_errs.ThrowInternal(nil, "TWILI-s0pLc", "message is not SMS")
|
||||
}
|
||||
m, err := 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
|
||||
})
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
package email
|
||||
package messages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/caos/zitadel/internal/notification/channels"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -11,7 +13,9 @@ var (
|
||||
lineBreak = "\r\n"
|
||||
)
|
||||
|
||||
type EmailMessage struct {
|
||||
var _ channels.Message = (*Email)(nil)
|
||||
|
||||
type Email struct {
|
||||
Recipients []string
|
||||
BCC []string
|
||||
CC []string
|
||||
@ -20,7 +24,7 @@ type EmailMessage struct {
|
||||
Content string
|
||||
}
|
||||
|
||||
func (msg *EmailMessage) GetContent() string {
|
||||
func (msg *Email) GetContent() string {
|
||||
headers := make(map[string]string)
|
||||
headers["From"] = msg.SenderEmail
|
||||
headers["To"] = strings.Join(msg.Recipients, ", ")
|
15
internal/notification/messages/sms.go
Normal file
15
internal/notification/messages/sms.go
Normal file
@ -0,0 +1,15 @@
|
||||
package messages
|
||||
|
||||
import "github.com/caos/zitadel/internal/notification/channels"
|
||||
|
||||
var _ channels.Message = (*SMS)(nil)
|
||||
|
||||
type SMS struct {
|
||||
SenderPhoneNumber string
|
||||
RecipientPhoneNumber string
|
||||
Content string
|
||||
}
|
||||
|
||||
func (msg *SMS) GetContent() string {
|
||||
return msg.Content
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package chat
|
||||
|
||||
type ChatConfig struct {
|
||||
Url string
|
||||
SplitCount int
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package chat
|
||||
|
||||
type ChatMessage struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (msg *ChatMessage) GetContent() string {
|
||||
return msg.Text
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
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
|
@ -1,5 +0,0 @@
|
||||
package providers
|
||||
|
||||
type Message interface {
|
||||
GetContent() string
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
// 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))
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package providers
|
||||
|
||||
type NotificationProvider interface {
|
||||
CanHandleMessage() bool
|
||||
HandleMessage() error
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package twilio
|
||||
|
||||
type TwilioMessage struct {
|
||||
SenderPhoneNumber string
|
||||
RecipientPhoneNumber string
|
||||
Content string
|
||||
}
|
||||
|
||||
func (msg *TwilioMessage) GetContent() string {
|
||||
return msg.Content
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
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
|
||||
}
|
24
internal/notification/senders/chain.go
Normal file
24
internal/notification/senders/chain.go
Normal file
@ -0,0 +1,24 @@
|
||||
package senders
|
||||
|
||||
import "github.com/caos/zitadel/internal/notification/channels"
|
||||
|
||||
var _ channels.NotificationChannel = (*Chain)(nil)
|
||||
|
||||
type Chain struct {
|
||||
channels []channels.NotificationChannel
|
||||
}
|
||||
|
||||
func chainChannels(channel ...channels.NotificationChannel) *Chain {
|
||||
return &Chain{channels: channel}
|
||||
}
|
||||
|
||||
// HandleMessage returns a non nil error from a provider immediately if any occurs
|
||||
// messages are sent to channels in the same order they were provided to chainChannels()
|
||||
func (c *Chain) HandleMessage(message channels.Message) error {
|
||||
for i := range c.channels {
|
||||
if err := c.channels[i].HandleMessage(message); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
46
internal/notification/senders/debug.go
Normal file
46
internal/notification/senders/debug.go
Normal file
@ -0,0 +1,46 @@
|
||||
package senders
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/notification/channels"
|
||||
"github.com/caos/zitadel/internal/notification/channels/chat"
|
||||
"github.com/caos/zitadel/internal/notification/channels/fs"
|
||||
"github.com/caos/zitadel/internal/notification/channels/log"
|
||||
)
|
||||
|
||||
func debugChannels(config systemdefaults.Notifications) (channels.NotificationChannel, error) {
|
||||
|
||||
var (
|
||||
providers []channels.NotificationChannel
|
||||
enableChat bool
|
||||
)
|
||||
|
||||
if config.Providers.Chat.Enabled != nil {
|
||||
enableChat = *config.Providers.Chat.Enabled
|
||||
} else {
|
||||
// ensures backward compatible configuration
|
||||
enableChat = config.DebugMode
|
||||
}
|
||||
|
||||
if enableChat {
|
||||
p, err := chat.InitChatChannel(config.Providers.Chat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providers = append(providers, p)
|
||||
}
|
||||
|
||||
if config.Providers.FileSystem.Enabled {
|
||||
p, err := fs.InitFSChannel(config.Providers.FileSystem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providers = append(providers, p)
|
||||
}
|
||||
|
||||
if config.Providers.Log.Enabled {
|
||||
providers = append(providers, log.InitStdoutChannel(config.Providers.Log))
|
||||
}
|
||||
|
||||
return chainChannels(providers...), nil
|
||||
}
|
25
internal/notification/senders/email.go
Normal file
25
internal/notification/senders/email.go
Normal file
@ -0,0 +1,25 @@
|
||||
package senders
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/notification/channels"
|
||||
"github.com/caos/zitadel/internal/notification/channels/smtp"
|
||||
)
|
||||
|
||||
func EmailChannels(config systemdefaults.Notifications) (channels.NotificationChannel, error) {
|
||||
|
||||
debug, err := debugChannels(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !config.DebugMode {
|
||||
p, err := smtp.InitSMTPChannel(config.Providers.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return chainChannels(debug, p), nil
|
||||
}
|
||||
|
||||
return debug, nil
|
||||
}
|
21
internal/notification/senders/sms.go
Normal file
21
internal/notification/senders/sms.go
Normal file
@ -0,0 +1,21 @@
|
||||
package senders
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/notification/channels"
|
||||
"github.com/caos/zitadel/internal/notification/channels/twilio"
|
||||
)
|
||||
|
||||
func SMSChannels(config systemdefaults.Notifications) (channels.NotificationChannel, error) {
|
||||
|
||||
debug, err := debugChannels(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !config.DebugMode {
|
||||
return chainChannels(debug, twilio.InitTwilioChannel(config.Providers.Twilio)), nil
|
||||
}
|
||||
|
||||
return debug, nil
|
||||
}
|
@ -3,21 +3,16 @@ package types
|
||||
import (
|
||||
"html"
|
||||
|
||||
"github.com/caos/zitadel/internal/notification/messages"
|
||||
"github.com/caos/zitadel/internal/notification/senders"
|
||||
|
||||
"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, subject, content string, config systemdefaults.Notifications, lastEmail bool) error {
|
||||
provider, err := email.InitEmailProvider(config.Providers.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content = html.UnescapeString(content)
|
||||
message := &email.EmailMessage{
|
||||
message := &messages.Email{
|
||||
SenderEmail: config.Providers.Email.From,
|
||||
Recipients: []string{user.VerifiedEmail},
|
||||
Subject: subject,
|
||||
@ -26,21 +21,13 @@ func generateEmail(user *view_model.NotifyUser, subject, content string, config
|
||||
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)
|
||||
channels, err := senders.EmailChannels(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return provider.HandleMessage(message)
|
||||
|
||||
return channels.HandleMessage(message)
|
||||
}
|
||||
|
||||
func mapNotifyUserToArgs(user *view_model.NotifyUser) map[string]interface{} {
|
||||
|
@ -2,16 +2,13 @@ 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"
|
||||
"github.com/caos/zitadel/internal/notification/messages"
|
||||
"github.com/caos/zitadel/internal/notification/senders"
|
||||
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{
|
||||
message := &messages.SMS{
|
||||
SenderPhoneNumber: config.Providers.Twilio.From,
|
||||
RecipientPhoneNumber: user.VerifiedPhone,
|
||||
Content: content,
|
||||
@ -19,19 +16,10 @@ func generateSms(user *view_model.NotifyUser, content string, config systemdefau
|
||||
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)
|
||||
channels, err := senders.SMSChannels(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return provider.HandleMessage(message)
|
||||
return channels.HandleMessage(message)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user