mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-27 22:27:23 +08:00
[FEAT] disable email delivery (#1419)
* feat: add config to disable email delivery * chore: update config schema * docs: add new config parameter * test: fix test * fix: rename email webhook event * docs: Update backend/docs/Config.md Co-authored-by: Lennart Fleischmann <67686424+lfleischmann@users.noreply.github.com> --------- Co-authored-by: Lennart Fleischmann <67686424+lfleischmann@users.noreply.github.com>
This commit is contained in:
@ -23,6 +23,7 @@ type Config struct {
|
||||
Server Server `yaml:"server" json:"server,omitempty" koanf:"server"`
|
||||
Webauthn WebauthnSettings `yaml:"webauthn" json:"webauthn,omitempty" koanf:"webauthn"`
|
||||
Smtp SMTP `yaml:"smtp" json:"smtp,omitempty" koanf:"smtp"`
|
||||
EmailDelivery EmailDelivery `yaml:"email_delivery" json:"email_delivery,omitempty" koanf:"email_delivery" split_words:"true"`
|
||||
Passcode Passcode `yaml:"passcode" json:"passcode" koanf:"passcode"`
|
||||
Password Password `yaml:"password" json:"password,omitempty" koanf:"password"`
|
||||
Database Database `yaml:"database" json:"database" koanf:"database"`
|
||||
@ -118,6 +119,9 @@ func DefaultConfig() *Config {
|
||||
Smtp: SMTP{
|
||||
Port: "465",
|
||||
},
|
||||
EmailDelivery: EmailDelivery{
|
||||
Enabled: true,
|
||||
},
|
||||
Passcode: Passcode{
|
||||
TTL: 300,
|
||||
Email: Email{
|
||||
@ -203,10 +207,12 @@ func (c *Config) Validate() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to validate webauthn settings: %w", err)
|
||||
}
|
||||
if c.EmailDelivery.Enabled {
|
||||
err = c.Smtp.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to validate smtp settings: %w", err)
|
||||
}
|
||||
}
|
||||
err = c.Passcode.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to validate passcode settings: %w", err)
|
||||
@ -382,6 +388,10 @@ func (s *SMTP) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type EmailDelivery struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled" jsonschema:"default=true"`
|
||||
}
|
||||
|
||||
type Email struct {
|
||||
FromAddress string `yaml:"from_address" json:"from_address,omitempty" koanf:"from_address" split_words:"true" jsonschema:"default=passcode@hanko.io"`
|
||||
FromName string `yaml:"from_name" json:"from_name,omitempty" koanf:"from_name" split_words:"true" jsonschema:"default=Hanko"`
|
||||
@ -688,6 +698,9 @@ func (c *Config) PostProcess() error {
|
||||
}
|
||||
|
||||
func (c *Config) arrangeSmtpSettings() {
|
||||
if !c.EmailDelivery.Enabled {
|
||||
return
|
||||
}
|
||||
if c.Passcode.Smtp.Validate() == nil {
|
||||
if c.Smtp.Validate() == nil {
|
||||
zeroLogger.Warn().Msg("Both root smtp and passcode.smtp are set. Using smtp settings from root configuration")
|
||||
|
||||
@ -300,6 +300,18 @@ webauthn:
|
||||
origins:
|
||||
- "android:apk-key-hash:nLSu7wVTbnMOxLgC52f2faTnv..."
|
||||
- "https://login.example.com"
|
||||
## email_delivery ##
|
||||
#
|
||||
# Settings needed for email delivery.
|
||||
#
|
||||
email_delivery:
|
||||
## enabled ##
|
||||
#
|
||||
# Enable or disable email delivery by hanko. Disable if you want to send the emails yourself. To send emails yourself you must subscribe to the `email.create` webhook event.
|
||||
#
|
||||
# Default: true
|
||||
#
|
||||
enabled: true
|
||||
## audit_log ##
|
||||
#
|
||||
# Configures audit logging
|
||||
@ -1011,4 +1023,10 @@ webhooks:
|
||||
# Email - Triggers on: change of primary email
|
||||
#
|
||||
- user.update.email.primary
|
||||
##
|
||||
#
|
||||
# Triggers on: an email was sent or should be sent
|
||||
#
|
||||
- email.send
|
||||
|
||||
```
|
||||
|
||||
26
backend/dto/webhook/email.go
Normal file
26
backend/dto/webhook/email.go
Normal file
@ -0,0 +1,26 @@
|
||||
package webhook
|
||||
|
||||
type EmailSend struct {
|
||||
Subject string `json:"subject"` // subject
|
||||
BodyPlain string `json:"body_plain"` // used for string templates
|
||||
Body string `json:"body,omitempty"` // used for html templates
|
||||
ToEmailAddress string `json:"to_email_address"`
|
||||
DeliveredByHanko bool `json:"delivered_by_hanko"`
|
||||
AcceptLanguage string `json:"accept_language"` // accept_language header from http request
|
||||
Type EmailType `json:"type"` // type of the email, currently only "passcode", but other could be added later
|
||||
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type PasscodeData struct {
|
||||
ServiceName string `json:"service_name"`
|
||||
OtpCode string `json:"otp_code"`
|
||||
TTL int `json:"ttl"`
|
||||
ValidUntil int64 `json:"valid_until"` // UnixTimestamp
|
||||
}
|
||||
|
||||
type EmailType string
|
||||
|
||||
var (
|
||||
EmailTypePasscode EmailType = "passcode"
|
||||
)
|
||||
@ -144,7 +144,7 @@ func (h *EmailHandler) Create(c echo.Context) error {
|
||||
var evt events.Event
|
||||
|
||||
if len(user.Emails) >= 1 {
|
||||
evt = events.EmailCreate
|
||||
evt = events.UserEmailCreate
|
||||
} else {
|
||||
evt = events.UserCreate
|
||||
}
|
||||
@ -212,7 +212,7 @@ func (h *EmailHandler) SetPrimaryEmail(c echo.Context) error {
|
||||
return fmt.Errorf("failed to create audit log: %w", err)
|
||||
}
|
||||
|
||||
utils.NotifyUserChange(c, tx, h.persister, events.EmailPrimary, userId)
|
||||
utils.NotifyUserChange(c, tx, h.persister, events.UserEmailPrimary, userId)
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
})
|
||||
@ -256,7 +256,7 @@ func (h *EmailHandler) Delete(c echo.Context) error {
|
||||
return fmt.Errorf("failed to create audit log: %w", err)
|
||||
}
|
||||
|
||||
utils.NotifyUserChange(c, tx, h.persister, events.EmailDelete, userId)
|
||||
utils.NotifyUserChange(c, tx, h.persister, events.UserEmailDelete, userId)
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
})
|
||||
|
||||
@ -150,7 +150,7 @@ func (h *emailAdminHandler) Create(ctx echo.Context) error {
|
||||
err = h.persister.GetPrimaryEmailPersisterWithConnection(tx).Create(*primaryEmail)
|
||||
}
|
||||
|
||||
utils.NotifyUserChange(ctx, tx, h.persister, events.EmailCreate, userId)
|
||||
utils.NotifyUserChange(ctx, tx, h.persister, events.UserEmailCreate, userId)
|
||||
|
||||
return ctx.JSON(http.StatusCreated, admin.FromEmailModel(email))
|
||||
})
|
||||
@ -229,7 +229,7 @@ func (h *emailAdminHandler) Delete(ctx echo.Context) error {
|
||||
return fmt.Errorf("failed to delete email from db: %w", err)
|
||||
}
|
||||
|
||||
utils.NotifyUserChange(ctx, tx, h.persister, events.EmailDelete, userId)
|
||||
utils.NotifyUserChange(ctx, tx, h.persister, events.UserEmailDelete, userId)
|
||||
|
||||
return ctx.NoContent(http.StatusNoContent)
|
||||
})
|
||||
@ -275,7 +275,7 @@ func (h *emailAdminHandler) SetPrimaryEmail(ctx echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.NotifyUserChange(ctx, tx, h.persister, events.EmailPrimary, userId)
|
||||
utils.NotifyUserChange(ctx, tx, h.persister, events.UserEmailPrimary, userId)
|
||||
|
||||
return ctx.NoContent(http.StatusNoContent)
|
||||
})
|
||||
|
||||
@ -7,11 +7,13 @@ import (
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
zeroLogger "github.com/rs/zerolog/log"
|
||||
"github.com/sethvargo/go-limiter"
|
||||
"github.com/teamhanko/hanko/backend/audit_log"
|
||||
"github.com/teamhanko/hanko/backend/config"
|
||||
"github.com/teamhanko/hanko/backend/crypto"
|
||||
"github.com/teamhanko/hanko/backend/dto"
|
||||
"github.com/teamhanko/hanko/backend/dto/webhook"
|
||||
"github.com/teamhanko/hanko/backend/mail"
|
||||
"github.com/teamhanko/hanko/backend/persistence"
|
||||
"github.com/teamhanko/hanko/backend/persistence/models"
|
||||
@ -189,24 +191,55 @@ func (h *PasscodeHandler) Init(c echo.Context) error {
|
||||
}
|
||||
|
||||
lang := c.Request().Header.Get("Accept-Language")
|
||||
str, err := h.renderer.Render("loginTextMail", lang, data)
|
||||
subject := h.renderer.Translate(lang, "email_subject_login", data)
|
||||
bodyPlain, err := h.renderer.Render("loginTextMail", lang, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render email template: %w", err)
|
||||
}
|
||||
|
||||
webhookData := webhook.EmailSend{
|
||||
Subject: subject,
|
||||
BodyPlain: bodyPlain,
|
||||
ToEmailAddress: email.Address,
|
||||
DeliveredByHanko: true,
|
||||
AcceptLanguage: lang,
|
||||
Type: webhook.EmailTypePasscode,
|
||||
Data: webhook.PasscodeData{
|
||||
ServiceName: h.cfg.Service.Name,
|
||||
OtpCode: passcode,
|
||||
TTL: h.TTL,
|
||||
ValidUntil: passcodeModel.CreatedAt.Add(time.Duration(h.TTL) * time.Second).UTC().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
if h.cfg.EmailDelivery.Enabled {
|
||||
message := gomail.NewMessage()
|
||||
message.SetAddressHeader("To", email.Address, "")
|
||||
message.SetAddressHeader("From", h.emailConfig.FromAddress, h.emailConfig.FromName)
|
||||
|
||||
message.SetHeader("Subject", h.renderer.Translate(lang, "email_subject_login", data))
|
||||
message.SetHeader("Subject", subject)
|
||||
|
||||
message.SetBody("text/plain", str)
|
||||
message.SetBody("text/plain", bodyPlain)
|
||||
|
||||
err = h.mailer.Send(message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send passcode: %w", err)
|
||||
}
|
||||
|
||||
err = utils.TriggerWebhooks(c, events.EmailSend, webhookData)
|
||||
|
||||
if err != nil {
|
||||
zeroLogger.Warn().Err(err).Msg("failed to trigger webhook")
|
||||
}
|
||||
} else {
|
||||
webhookData.DeliveredByHanko = false
|
||||
err = utils.TriggerWebhooks(c, events.EmailSend, webhookData)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf(fmt.Sprintf("failed to trigger webhook: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
err = h.auditLogger.Create(c, models.AuditLogPasscodeLoginInitSucceeded, user, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create audit log: %w", err)
|
||||
@ -326,7 +359,7 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
|
||||
}
|
||||
|
||||
wasUnverified := false
|
||||
hasEmails := len(user.Emails) >= 1 // check if we need to trigger a UserCreate webhook or a EmailCreate one
|
||||
hasEmails := len(user.Emails) >= 1 // check if we need to trigger a UserCreate webhook or a UserEmailCreate one
|
||||
|
||||
if !passcode.Email.Verified {
|
||||
wasUnverified = true
|
||||
@ -394,7 +427,7 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
|
||||
var evt events.Event
|
||||
|
||||
if hasEmails {
|
||||
evt = events.EmailCreate
|
||||
evt = events.UserEmailCreate
|
||||
} else {
|
||||
evt = events.UserCreate
|
||||
}
|
||||
|
||||
@ -143,9 +143,9 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister, promet
|
||||
webauthnCredentials.DELETE("/:id", webauthnHandler.DeleteCredential)
|
||||
|
||||
passcode := g.Group("/passcode")
|
||||
passcodeLogin := passcode.Group("/login")
|
||||
passcodeLogin := passcode.Group("/login", webhookMiddlware)
|
||||
passcodeLogin.POST("/initialize", passcodeHandler.Init)
|
||||
passcodeLogin.POST("/finalize", passcodeHandler.Finish, webhookMiddlware)
|
||||
passcodeLogin.POST("/finalize", passcodeHandler.Finish)
|
||||
|
||||
email := g.Group("/emails", sessionMiddleware, webhookMiddlware)
|
||||
email.GET("", emailHandler.List)
|
||||
|
||||
@ -133,6 +133,9 @@
|
||||
"smtp": {
|
||||
"$ref": "#/$defs/SMTP"
|
||||
},
|
||||
"email_delivery": {
|
||||
"$ref": "#/$defs/EmailDelivery"
|
||||
},
|
||||
"passcode": {
|
||||
"$ref": "#/$defs/Passcode"
|
||||
},
|
||||
@ -302,6 +305,19 @@
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"EmailDelivery": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enabled"
|
||||
]
|
||||
},
|
||||
"Emails": {
|
||||
"properties": {
|
||||
"require_verification": {
|
||||
|
||||
@ -53,7 +53,6 @@ func Limit(store limiter.Store, userId uuid.UUID, c echo.Context) error {
|
||||
}
|
||||
|
||||
resetTime := int(math.Floor(time.Unix(0, int64(reset)).UTC().Sub(time.Now().UTC()).Seconds()))
|
||||
log.Println(resetTime)
|
||||
|
||||
// Set headers (we do this regardless of whether the request is permitted).
|
||||
c.Response().Header().Set(httplimit.HeaderRateLimitLimit, strconv.FormatUint(limit, 10))
|
||||
|
||||
@ -20,6 +20,9 @@ var DefaultConfig = config.Config{
|
||||
Host: "localhost",
|
||||
Port: "2500",
|
||||
},
|
||||
EmailDelivery: config.EmailDelivery{
|
||||
Enabled: true,
|
||||
},
|
||||
Passcode: config.Passcode{
|
||||
Email: config.Email{
|
||||
FromAddress: "test@hanko.io",
|
||||
|
||||
2
backend/thirdparty/linking.go
vendored
2
backend/thirdparty/linking.go
vendored
@ -128,7 +128,7 @@ func signIn(tx *pop.Connection, cfg *config.Config, p persistence.Persister, use
|
||||
}
|
||||
|
||||
identity.EmailID = email.ID
|
||||
webhookEvent = events.EmailCreate
|
||||
webhookEvent = events.UserEmailCreate
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,10 +9,12 @@ const (
|
||||
UserCreate Event = "user.create"
|
||||
UserUpdate Event = "user.update"
|
||||
UserDelete Event = "user.delete"
|
||||
Email Event = "user.update.email"
|
||||
EmailCreate Event = "user.update.email.create"
|
||||
EmailPrimary Event = "user.update.email.primary"
|
||||
EmailDelete Event = "user.update.email.delete"
|
||||
UserEmail Event = "user.update.email"
|
||||
UserEmailCreate Event = "user.update.email.create"
|
||||
UserEmailPrimary Event = "user.update.email.primary"
|
||||
UserEmailDelete Event = "user.update.email.delete"
|
||||
|
||||
EmailSend Event = "email.send"
|
||||
)
|
||||
|
||||
func StringIsValidEvent(value string) bool {
|
||||
@ -23,7 +25,7 @@ func StringIsValidEvent(value string) bool {
|
||||
func IsValidEvent(evt Event) bool {
|
||||
var isValid bool
|
||||
switch evt {
|
||||
case User, UserCreate, UserUpdate, UserDelete, Email, EmailCreate, EmailPrimary, EmailDelete:
|
||||
case User, UserCreate, UserUpdate, UserDelete, UserEmail, UserEmailCreate, UserEmailPrimary, UserEmailDelete, EmailSend:
|
||||
isValid = true
|
||||
default:
|
||||
isValid = false
|
||||
|
||||
@ -17,7 +17,7 @@ func TestBaseWebhook_HasEvent(t *testing.T) {
|
||||
Events: events.Events{events.UserUpdate},
|
||||
}
|
||||
|
||||
require.True(t, baseHook.HasEvent(events.EmailCreate))
|
||||
require.True(t, baseHook.HasEvent(events.UserEmailCreate))
|
||||
}
|
||||
|
||||
func TestWebhooks_HasEvent_WithMultipleEvents(t *testing.T) {
|
||||
@ -37,7 +37,7 @@ func TestWebhooks_HasSubEvent_WithMultipleEvents(t *testing.T) {
|
||||
Events: events.Events{events.UserCreate, events.UserUpdate},
|
||||
}
|
||||
|
||||
require.True(t, baseHook.HasEvent(events.EmailCreate))
|
||||
require.True(t, baseHook.HasEvent(events.UserEmailCreate))
|
||||
}
|
||||
|
||||
func TestBaseWebhook_HasSubEvent(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user