Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
149 lines
4.0 KiB
Go
149 lines
4.0 KiB
Go
// Copyright © 2026 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package hook
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"slices"
|
|
|
|
"github.com/ory/kratos/driver/config"
|
|
"github.com/ory/kratos/identity"
|
|
"github.com/ory/kratos/selfservice/flow/settings"
|
|
"github.com/ory/x/logrusx"
|
|
"github.com/ory/x/otelx"
|
|
)
|
|
|
|
const (
|
|
RecipientsRemoved = "removed"
|
|
RecipientsAllVerified = "all_verified"
|
|
RecipientsAll = "all"
|
|
)
|
|
|
|
type (
|
|
NotifyPreviousAddressesConfig struct {
|
|
Recipients string `json:"recipients"`
|
|
}
|
|
|
|
notifyPreviousAddressesDependencies interface {
|
|
identity.ManagementProvider
|
|
config.Provider
|
|
logrusx.Provider
|
|
otelx.Provider
|
|
}
|
|
|
|
NotifyPreviousAddresses struct {
|
|
r notifyPreviousAddressesDependencies
|
|
c *NotifyPreviousAddressesConfig
|
|
}
|
|
)
|
|
|
|
var _ settings.PostHookPostPersistExecutor = new(NotifyPreviousAddresses)
|
|
|
|
func NewNotifyPreviousAddresses(r notifyPreviousAddressesDependencies, c *NotifyPreviousAddressesConfig) *NotifyPreviousAddresses {
|
|
if c == nil {
|
|
c = &NotifyPreviousAddressesConfig{}
|
|
}
|
|
if c.Recipients == "" {
|
|
c.Recipients = RecipientsRemoved
|
|
}
|
|
return &NotifyPreviousAddresses{r: r, c: c}
|
|
}
|
|
|
|
func (e *NotifyPreviousAddresses) ExecuteSettingsPostPersistHook(
|
|
_ http.ResponseWriter, r *http.Request,
|
|
params settings.PostHookPostPersistExecutorParams,
|
|
) error {
|
|
return otelx.WithSpan(r.Context(), "selfservice.hook.NotifyPreviousAddresses.ExecuteSettingsPostPersistHook", func(ctx context.Context) error {
|
|
if params.Previous == nil || params.Updated == nil {
|
|
return nil
|
|
}
|
|
targets := computeNotifyTargets(e.c.Recipients, params.Previous, params.Updated)
|
|
targets = slices.DeleteFunc(targets, func(a identity.AddressRef) bool {
|
|
return a.Via != identity.AddressTypeEmail && a.Via != identity.AddressTypeSMS
|
|
})
|
|
if len(targets) == 0 {
|
|
return nil
|
|
}
|
|
if err := e.r.IdentityManager().SendVerifiableAddressChangedNotifications(ctx, targets, params.Updated); err != nil {
|
|
e.r.Logger().WithError(err).
|
|
WithField("count", len(targets)).
|
|
Warn("Failed to queue one or more verifiable-address-change notifications.")
|
|
// Persist already succeeded — never fail the flow on courier errors.
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func computeNotifyTargets(mode string, original, proposed *identity.Identity) []identity.AddressRef {
|
|
if !addressesChanged(original.VerifiableAddresses, proposed.VerifiableAddresses) {
|
|
return nil
|
|
}
|
|
switch mode {
|
|
case RecipientsAllVerified:
|
|
out := make([]identity.AddressRef, 0, len(original.VerifiableAddresses))
|
|
for _, a := range original.VerifiableAddresses {
|
|
if a.Verified {
|
|
out = append(out, identity.AddressRef{Value: a.Value, Via: a.Via})
|
|
}
|
|
}
|
|
return out
|
|
case RecipientsAll:
|
|
out := make([]identity.AddressRef, 0, len(original.VerifiableAddresses))
|
|
for _, a := range original.VerifiableAddresses {
|
|
out = append(out, identity.AddressRef{Value: a.Value, Via: a.Via})
|
|
}
|
|
return out
|
|
default: // RecipientsRemoved
|
|
keep := map[string]struct{}{}
|
|
for _, a := range proposed.VerifiableAddresses {
|
|
keep[a.Via+"|"+a.Value] = struct{}{}
|
|
}
|
|
out := make([]identity.AddressRef, 0)
|
|
for _, a := range original.VerifiableAddresses {
|
|
if _, still := keep[a.Via+"|"+a.Value]; still {
|
|
continue
|
|
}
|
|
out = append(out, identity.AddressRef{Value: a.Value, Via: a.Via})
|
|
}
|
|
return out
|
|
}
|
|
}
|
|
|
|
type set struct {
|
|
m map[string]struct{}
|
|
}
|
|
|
|
func newSet() *set {
|
|
return &set{m: make(map[string]struct{})}
|
|
}
|
|
|
|
func (s *set) add(via, value string) {
|
|
s.m[via+"|"+value] = struct{}{}
|
|
}
|
|
|
|
func (s *set) contains(via, value string) bool {
|
|
_, ok := s.m[via+"|"+value]
|
|
return ok
|
|
}
|
|
|
|
// addressesChanged reports whether two verifiable-address slices represent
|
|
// different (via,value) sets. Status or verification-flag flips alone are not
|
|
// "address changes" and must not trigger notifications.
|
|
func addressesChanged(a, b []identity.VerifiableAddress) bool {
|
|
if len(a) != len(b) {
|
|
return true
|
|
}
|
|
seen := newSet()
|
|
for _, x := range a {
|
|
seen.add(x.Via, x.Value)
|
|
}
|
|
for _, x := range b {
|
|
if !seen.contains(x.Via, x.Value) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|