zitadel/internal/notification/repository/eventsourcing/handler/notification.go
Fabi 73d37459bb
feat: label policy (#1708)
* feat: label policy proto extension

* feat: label policy and activate event

* feat: label policy asset events

* feat: label policy asset commands

* feat: add storage key

* feat: storage key validation

* feat: label policy asset tests

* feat: label policy query side

* feat: avatar

* feat: avatar event

* feat: human avatar

* feat: avatar read side

* feat: font on iam label policy

* feat: label policy font

* feat: possiblity to create bucket on put file

* uplaoder

* login policy logo

* set bucket prefix

* feat: avatar upload

* feat: avatar upload

* feat: use assets on command side

* feat: fix human avatar removed event

* feat: remove human avatar

* feat: mock asset storage

* feat: remove human avatar

* fix(operator): add configuration of asset storage to zitadel operator

* feat(console): private labeling policy (#1697)

* private labeling component, routing, preview

* font, colors, upload, i18n

* show logo

* fix: uniqueness (#1710)

* fix: uniqueconstraint to lower

* feat: change org

* feat: org change test

* feat: change org

* fix: tests

* fix: handle domain claims correctly

* feat: update org

Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>

* fix: handle domain claimed event correctly for service users (#1711)

* fix: handle domain claimed event correctly on user view

* fix: ignore domain claimed events for email notifications

* fix: change org

* handle org changed in read models correctly

* fix: change org in user grant handler

Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>

* fix: correct value (#1695)

* docs(api): correct link (#1712)

* upload service

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>
Co-authored-by: Florian Forster <florian@caos.ch>

* feat: fix tests,

* feat: remove assets from label policy

* fix npm, set environment

* lint ts

* remove stylelinting

* fix(operator): add mapping for console with changed unit tests

* fix(operator): add secrets as env variables to pod

* feat: remove human avatar

* fix(operator): add secrets as env variables to pod

* feat: map label policy

* feat: labelpolicy, admin, mgmt, adv settings (#1715)

* fetch label policy, mgmt, admin service

* feat: advanced beh, links, add, update

* lint ts

* feat: watermark

* feat: remove human avatar

* feat: remove human avatar

* feat: remove human avatar

* feat: remove human avatar

* feat: remove human avatar

* feat: remove human avatar

* feat: remove human avatar

* feat: custom css

* css

* css

* css

* css

* css

* getobject

* feat: dynamic handler

* feat: varibale css

* content info

* css overwrite

* feat: variablen css

* feat: generate css file

* feat: dark mode

* feat: dark mode

* fix logo css

* feat: upload logos

* dark mode with cookie

* feat: handle images in login

* avatar css and begin font

* feat: avatar

* feat: user avatar

* caching of static assets in login

* add avatar.js to main.html

* feat: header dont show logo if no url

* feat: label policy colors

* feat: mock asset storage

* feat: mock asset storage

* feat: fix tests

* feat: user avatar

* feat: header logo

* avatar

* avatar

* make it compatible with go 1.15

* feat: remove unused logos

* fix handler

* fix: styling error handling

* fonts

* fix: download func

* switch to mux

* fix: change upload api to assets

* fix build

* fix: download avatar

* fix: download logos

* fix: my avatar

* font

* fix: remove error msg popup possibility

* fix: docs

* fix: svalidate colors

* rem msg popup from frontend

* fix: email with private labeling

* fix: tests

* fix: email templates

* fix: change migration version

* fix: fix duplicate imports

* fix(console): assets, service url, upload, policy current and preview  (#1781)

* upload endpoint, layout

* fetch current, preview, fix upload

* cleanup private labeling

* fix linting

* begin generated asset handler

* generate asset api in dockerfile

* features for label policy

* features for label policy

* features

* flag for asset generator

* change asset generator flag

* fix label policy view in grpc

* fix: layout, activate policy (#1786)

* theme switcher up on top

* change layout

* activate policy

* feat(console): label policy back color, layout (#1788)

* theme switcher up on top

* change layout

* activate policy

* fix overwrite value fc

* reset policy, reset service

* autosave policy, preview desc, layout impv

* layout, i18n

* background colors, inject material styles

* load images

* clean, lint

* fix layout

* set custom hex

* fix content size conversion

* remove font format in generated css

* fix features for assets

* fix(console): label policy colors, image downloads, preview (#1804)

* load images

* colors, images binding

* lint

* refresh emitter

* lint

* propagate font colors

* upload error handling

* label policy feature check

* add blob in csp for console

* log

* fix: feature edits for label policy, refresh state on upload (#1807)

* show error on load image, stop spinner

* fix merge

* fix migration versions

* fix assets

* fix csp

* fix background color

* scss

* fix build

* lint scss

* fix statik for console

* fix features check for label policy

* cleanup

* lint

* public links

* fix notifications

* public links

* feat: merge main

* feat: fix translation files

* fix migration

* set api domain

* fix logo in email

* font face in email

* font face in email

* validate assets on upload

* cleanup

* add missing translations

* add missing translations

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Stefan Benz <stefan@caos.ch>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Florian Forster <florian@caos.ch>
2021-06-04 14:53:51 +02:00

443 lines
13 KiB
Go

package handler
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/caos/logging"
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/eventstore/v1/query"
"github.com/caos/zitadel/internal/eventstore/v1/spooler"
"github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model"
"github.com/caos/zitadel/internal/notification/types"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/user/repository/view"
"github.com/caos/zitadel/internal/user/repository/view/model"
)
const (
notificationTable = "notification.notifications"
NotifyUserID = "NOTIFICATION"
labelPolicyTableOrg = "management.label_policies"
labelPolicyTableDef = "adminapi.label_policies"
mailTemplateTableOrg = "management.mail_templates"
mailTemplateTableDef = "adminapi.mail_templates"
mailTextTableOrg = "management.mail_texts"
mailTextTableDef = "adminapi.mail_texts"
mailTextTypeDomainClaimed = "DomainClaimed"
mailTextTypeInitCode = "InitCode"
mailTextTypePasswordReset = "PasswordReset"
mailTextTypeVerifyEmail = "VerifyEmail"
mailTextTypeVerifyPhone = "VerifyPhone"
)
type Notification struct {
handler
command *command.Commands
systemDefaults sd.SystemDefaults
AesCrypto crypto.EncryptionAlgorithm
i18n *i18n.Translator
statikDir http.FileSystem
subscription *v1.Subscription
apiDomain string
}
func newNotification(
handler handler,
command *command.Commands,
defaults sd.SystemDefaults,
aesCrypto crypto.EncryptionAlgorithm,
translator *i18n.Translator,
statikDir http.FileSystem,
apiDomain string,
) *Notification {
h := &Notification{
handler: handler,
command: command,
systemDefaults: defaults,
i18n: translator,
statikDir: statikDir,
AesCrypto: aesCrypto,
apiDomain: apiDomain,
}
h.subscribe()
return h
}
func (k *Notification) subscribe() {
k.subscription = k.es.Subscribe(k.AggregateTypes()...)
go func() {
for event := range k.subscription.Events {
query.ReduceEvent(k, event)
}
}()
}
func (n *Notification) ViewModel() string {
return notificationTable
}
func (_ *Notification) AggregateTypes() []models.AggregateType {
return []models.AggregateType{es_model.UserAggregate}
}
func (n *Notification) CurrentSequence() (uint64, error) {
sequence, err := n.view.GetLatestNotificationSequence()
if err != nil {
return 0, err
}
return sequence.CurrentSequence, nil
}
func (n *Notification) EventQuery() (*models.SearchQuery, error) {
sequence, err := n.view.GetLatestNotificationSequence()
if err != nil {
return nil, err
}
return view.UserQuery(sequence.CurrentSequence), nil
}
func (n *Notification) Reduce(event *models.Event) (err error) {
switch event.Type {
case es_model.InitializedUserCodeAdded,
es_model.InitializedHumanCodeAdded:
err = n.handleInitUserCode(event)
case es_model.UserEmailCodeAdded,
es_model.HumanEmailCodeAdded:
err = n.handleEmailVerificationCode(event)
case es_model.UserPhoneCodeAdded,
es_model.HumanPhoneCodeAdded:
err = n.handlePhoneVerificationCode(event)
case es_model.UserPasswordCodeAdded,
es_model.HumanPasswordCodeAdded:
err = n.handlePasswordCode(event)
case es_model.DomainClaimed:
err = n.handleDomainClaimed(event)
}
if err != nil {
return err
}
return n.view.ProcessedNotificationSequence(event)
}
func (n *Notification) handleInitUserCode(event *models.Event) (err error) {
initCode := new(es_model.InitUserCode)
if err := initCode.SetData(event); err != nil {
return err
}
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(event, initCode.Expiry,
es_model.InitializedUserCodeAdded, es_model.InitializedUserCodeSent,
es_model.InitializedHumanCodeAdded, es_model.InitializedHumanCodeSent)
if err != nil || alreadyHandled {
return err
}
ctx := getSetNotifyContextData(event.ResourceOwner)
colors, err := n.getLabelPolicy(ctx)
if err != nil {
return err
}
template, err := n.getMailTemplate(ctx)
if err != nil {
return err
}
user, err := n.getUserByID(event.AggregateID)
if err != nil {
return err
}
text, err := n.getMailText(ctx, mailTextTypeInitCode, user.PreferredLanguage)
if err != nil {
return err
}
err = types.SendUserInitCode(string(template.Template), text, user, initCode, n.systemDefaults, n.AesCrypto, colors, n.apiDomain)
if err != nil {
return err
}
return n.command.HumanInitCodeSent(ctx, event.ResourceOwner, event.AggregateID)
}
func (n *Notification) handlePasswordCode(event *models.Event) (err error) {
pwCode := new(es_model.PasswordCode)
if err := pwCode.SetData(event); err != nil {
return err
}
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(event, pwCode.Expiry,
es_model.UserPasswordCodeAdded, es_model.UserPasswordCodeSent,
es_model.HumanPasswordCodeAdded, es_model.HumanPasswordCodeSent)
if err != nil || alreadyHandled {
return err
}
ctx := getSetNotifyContextData(event.ResourceOwner)
colors, err := n.getLabelPolicy(ctx)
if err != nil {
return err
}
template, err := n.getMailTemplate(ctx)
if err != nil {
return err
}
user, err := n.getUserByID(event.AggregateID)
if err != nil {
return err
}
text, err := n.getMailText(ctx, mailTextTypePasswordReset, user.PreferredLanguage)
if err != nil {
return err
}
err = types.SendPasswordCode(string(template.Template), text, user, pwCode, n.systemDefaults, n.AesCrypto, colors, n.apiDomain)
if err != nil {
return err
}
return n.command.PasswordCodeSent(ctx, event.ResourceOwner, event.AggregateID)
}
func (n *Notification) handleEmailVerificationCode(event *models.Event) (err error) {
emailCode := new(es_model.EmailCode)
if err := emailCode.SetData(event); err != nil {
return err
}
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(event, emailCode.Expiry,
es_model.UserEmailCodeAdded, es_model.UserEmailCodeSent,
es_model.HumanEmailCodeAdded, es_model.HumanEmailCodeSent)
if err != nil || alreadyHandled {
return nil
}
ctx := getSetNotifyContextData(event.ResourceOwner)
colors, err := n.getLabelPolicy(ctx)
if err != nil {
return err
}
template, err := n.getMailTemplate(ctx)
if err != nil {
return err
}
user, err := n.getUserByID(event.AggregateID)
if err != nil {
return err
}
text, err := n.getMailText(ctx, mailTextTypeVerifyEmail, user.PreferredLanguage)
if err != nil {
return err
}
err = types.SendEmailVerificationCode(string(template.Template), text, user, emailCode, n.systemDefaults, n.AesCrypto, colors, n.apiDomain)
if err != nil {
return err
}
return n.command.HumanEmailVerificationCodeSent(ctx, event.ResourceOwner, event.AggregateID)
}
func (n *Notification) handlePhoneVerificationCode(event *models.Event) (err error) {
phoneCode := new(es_model.PhoneCode)
if err := phoneCode.SetData(event); err != nil {
return err
}
alreadyHandled, err := n.checkIfCodeAlreadyHandledOrExpired(event, phoneCode.Expiry,
es_model.UserPhoneCodeAdded, es_model.UserPhoneCodeSent,
es_model.HumanPhoneCodeAdded, es_model.HumanPhoneCodeSent)
if err != nil || alreadyHandled {
return nil
}
user, err := n.getUserByID(event.AggregateID)
if err != nil {
return err
}
err = types.SendPhoneVerificationCode(n.i18n, user, phoneCode, n.systemDefaults, n.AesCrypto)
if err != nil {
return err
}
return n.command.HumanPhoneVerificationCodeSent(getSetNotifyContextData(event.ResourceOwner), event.ResourceOwner, event.AggregateID)
}
func (n *Notification) handleDomainClaimed(event *models.Event) (err error) {
alreadyHandled, err := n.checkIfAlreadyHandled(event.AggregateID, event.Sequence, es_model.DomainClaimed, es_model.DomainClaimedSent)
if err != nil || alreadyHandled {
return nil
}
data := make(map[string]string)
if err := json.Unmarshal(event.Data, &data); err != nil {
logging.Log("HANDLE-Gghq2").WithError(err).Error("could not unmarshal event data")
return errors.ThrowInternal(err, "HANDLE-7hgj3", "could not unmarshal event")
}
user, err := n.getUserByID(event.AggregateID)
if err != nil {
return err
}
if user.LastEmail == "" {
return nil
}
ctx := getSetNotifyContextData(event.ResourceOwner)
colors, err := n.getLabelPolicy(ctx)
if err != nil {
return err
}
template, err := n.getMailTemplate(ctx)
if err != nil {
return err
}
text, err := n.getMailText(ctx, mailTextTypeDomainClaimed, user.PreferredLanguage)
if err != nil {
return err
}
err = types.SendDomainClaimed(string(template.Template), text, user, data["userName"], n.systemDefaults, colors, n.apiDomain)
if err != nil {
return err
}
return n.command.UserDomainClaimedSent(ctx, event.ResourceOwner, event.AggregateID)
}
func (n *Notification) checkIfCodeAlreadyHandledOrExpired(event *models.Event, expiry time.Duration, eventTypes ...models.EventType) (bool, error) {
if event.CreationDate.Add(expiry).Before(time.Now().UTC()) {
return true, nil
}
return n.checkIfAlreadyHandled(event.AggregateID, event.Sequence, eventTypes...)
}
func (n *Notification) checkIfAlreadyHandled(userID string, sequence uint64, eventTypes ...models.EventType) (bool, error) {
events, err := n.getUserEvents(userID, sequence)
if err != nil {
return false, err
}
for _, event := range events {
for _, eventType := range eventTypes {
if event.Type == eventType {
return true, nil
}
}
}
return false, nil
}
func (n *Notification) getUserEvents(userID string, sequence uint64) ([]*models.Event, error) {
query, err := view.UserByIDQuery(userID, sequence)
if err != nil {
return nil, err
}
return n.es.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 (n *Notification) OnSuccess() error {
return spooler.HandleSuccess(n.view.UpdateNotificationSpoolerRunTimestamp)
}
func getSetNotifyContextData(orgID string) context.Context {
return authz.SetCtxData(context.Background(), authz.CtxData{UserID: NotifyUserID, OrgID: orgID})
}
// Read organization specific colors
func (n *Notification) getLabelPolicy(ctx context.Context) (*iam_model.LabelPolicyView, error) {
// read from Org
policy, err := n.view.LabelPolicyByAggregateIDAndState(authz.GetCtxData(ctx).OrgID, labelPolicyTableOrg, int32(domain.LabelPolicyStateActive))
if errors.IsNotFound(err) {
// read from default
policy, err = n.view.LabelPolicyByAggregateIDAndState(n.systemDefaults.IamID, labelPolicyTableDef, int32(domain.LabelPolicyStateActive))
if err != nil {
return nil, err
}
policy.Default = true
}
if err != nil {
return nil, err
}
return iam_es_model.LabelPolicyViewToModel(policy), err
}
// Read organization specific template
func (n *Notification) getMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error) {
// read from Org
template, err := n.view.MailTemplateByAggregateID(authz.GetCtxData(ctx).OrgID, mailTemplateTableOrg)
if errors.IsNotFound(err) {
// read from default
template, err = n.view.MailTemplateByAggregateID(n.systemDefaults.IamID, mailTemplateTableDef)
if err != nil {
return nil, err
}
template.Default = true
}
if err != nil {
return nil, err
}
return iam_es_model.MailTemplateViewToModel(template), err
}
// Read organization specific texts
func (n *Notification) getMailText(ctx context.Context, textType string, lang string) (*iam_model.MailTextView, error) {
langTag := language.Make(lang)
if langTag == language.Und {
langTag = n.systemDefaults.DefaultLanguage
}
base, _ := langTag.Base()
// read from Org
mailText, err := n.view.MailTextByIDs(authz.GetCtxData(ctx).OrgID, textType, base.String(), mailTextTableOrg)
if errors.IsNotFound(err) {
// read from default
mailText, err = n.view.MailTextByIDs(n.systemDefaults.IamID, textType, base.String(), mailTextTableDef)
if err != nil {
return nil, err
}
mailText.Default = true
}
if err != nil {
return nil, err
}
return iam_es_model.MailTextViewToModel(mailText), err
}
func (n *Notification) getUserByID(userID string) (*model.NotifyUser, error) {
user, usrErr := n.view.NotifyUserByID(userID)
if usrErr != nil && !errors.IsNotFound(usrErr) {
return nil, usrErr
}
if user == nil {
user = &model.NotifyUser{}
}
events, err := n.getUserEvents(userID, user.Sequence)
if err != nil {
return user, usrErr
}
userCopy := *user
for _, event := range events {
if err := userCopy.AppendEvent(event); err != nil {
return user, nil
}
}
if userCopy.State == int32(model.UserStateDeleted) {
return nil, errors.ThrowNotFound(nil, "HANDLER-3n8fs", "Errors.User.NotFound")
}
return &userCopy, nil
}