mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
Base sends no auth mail, because it has no auth flow to send it for
The verification and email-change templates were the last of local auth. Their
endpoints went in the IAM-native rip — bindRecordAuthApi keeps auth-methods and
auth-refresh and nothing else — so both answered 404 for every deployment, not
only IAM ones, and their buttons addressed a route in an admin SPA that no
longer exists. Re-pathing them last commit made them honest; this removes them,
along with everything that existed only to serve them.
That turned out to be a lot, and each piece was already dead on its own terms:
core.EmailTemplate and the two collection fields — no other user
the mails package entire — its two functions were the only callers of its
own template resolver and its own html layout
the $mails JSVM namespace — an empty object once the two binds went, and its
type declarations advertised FIVE helpers, three of which (password reset,
OTP, auth alert) were removed in that same rip and never bound at all, so
hook authors have been reading a contract for functions that do not exist
OnMailerRecordVerificationSend / OnMailerRecordEmailChangeSend — nothing has
been able to fire either since the mails package was their only caller
core.MailerRecordEvent — the type those two hooks carried
Nothing loses a capability. $app.newMailClient().send() is the general sender,
and it is what an app binding its own confirm route would have used anyway; the
two removed binds were convenience wrappers over it.
POST /v1/settings/test/email stays and gets better. It rendered a template
against a throwaway record, so "can this deployment deliver mail" was answerable
only through a flow that no longer exists. It sends a plain message now — no
template, no collection, no record, no token — which is the question an operator
has after editing SMTP settings.
The collection fields are DEFAULTS applied at creation; a collection stores its
own copy, and an options key the struct no longer declares is ignored on read,
so nothing already deployed changes shape. types.d.ts is regenerated by its own
generator rather than hand-edited (the generator still listed the deleted
package, which is why it had to be fixed first).
This commit is contained in:
@@ -342,6 +342,27 @@ Store keys: `StoreKeyExternalAuthOnly` (always true once platform
|
||||
registers), `StoreKeyJWKSURL` (external mode), `StoreKeyAuthUsersCollection`
|
||||
(default `"users"`).
|
||||
|
||||
## Base sends no auth mail
|
||||
|
||||
There is no verification or email-change mail, no template for either on a
|
||||
collection, and no `mails` package. The endpoints those mails pointed at were
|
||||
deleted in the IAM-native rip — `bindRecordAuthApi` keeps `auth-methods` and
|
||||
`auth-refresh` and nothing else — so the templates described flows that answer
|
||||
404 for every deployment, and their buttons addressed a route in an admin SPA
|
||||
that no longer exists.
|
||||
|
||||
What went with them, because each existed only to serve those two mails:
|
||||
`core.EmailTemplate` and its two collection fields, the `$mails` JSVM namespace
|
||||
(whose type declarations advertised five helpers, three of which were removed in
|
||||
that same rip and never bound), the `OnMailerRecordVerificationSend` and
|
||||
`OnMailerRecordEmailChangeSend` hooks — which nothing could fire — and
|
||||
`core.MailerRecordEvent`.
|
||||
|
||||
Sending mail is untouched: `$app.newMailClient().send()` is the general sender an
|
||||
app's own hook uses, SMTP settings stay, and `POST /v1/settings/test/email` still
|
||||
proves they work — it sends a plain message now rather than rendering a template
|
||||
for a flow that is gone, which is the question an operator actually has.
|
||||
|
||||
## Mount prefix (`BASE_API_PREFIX`)
|
||||
|
||||
One knob for where the app's data plane lives. Default `/v1`. For
|
||||
|
||||
+12
-51
@@ -364,16 +364,14 @@ func TestSettingsTestEmail(t *testing.T) {
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"email":{"code":"validation_required"`,
|
||||
`"template":{"code":"validation_required"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "authorized as superuser (verifiation template)",
|
||||
Name: "authorized as superuser",
|
||||
Method: http.MethodPost,
|
||||
URL: "/v1/settings/test/email",
|
||||
Body: strings.NewReader(`{
|
||||
"template": "verification",
|
||||
"email": "[email protected]"
|
||||
}`),
|
||||
Headers: map[string]string{
|
||||
@@ -381,63 +379,26 @@ func TestSettingsTestEmail(t *testing.T) {
|
||||
},
|
||||
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||
if app.TestMailer.TotalSend() != 1 {
|
||||
t.Fatalf("[verification] Expected 1 sent email, got %d", app.TestMailer.TotalSend())
|
||||
t.Fatalf("Expected 1 sent email, got %d", app.TestMailer.TotalSend())
|
||||
}
|
||||
|
||||
if len(app.TestMailer.LastMessage().To) != 1 {
|
||||
t.Fatalf("[verification] Expected 1 recipient, got %v", app.TestMailer.LastMessage().To)
|
||||
msg := app.TestMailer.LastMessage()
|
||||
|
||||
if len(msg.To) != 1 || msg.To[0].Address != "[email protected]" {
|
||||
t.Fatalf("Expected the email sent to [email protected], got %v", msg.To)
|
||||
}
|
||||
|
||||
if app.TestMailer.LastMessage().To[0].Address != "[email protected]" {
|
||||
t.Fatalf("[verification] Expected the email to be sent to %s, got %s", "[email protected]", app.TestMailer.LastMessage().To[0].Address)
|
||||
}
|
||||
|
||||
if !strings.Contains(app.TestMailer.LastMessage().HTML, "Verify") {
|
||||
t.Fatalf("[verification] Expected to sent a verification email, got \n%v\n%v", app.TestMailer.LastMessage().Subject, app.TestMailer.LastMessage().HTML)
|
||||
// The send proves SMTP reaches the address and nothing else —
|
||||
// there is no flow behind it to complete, so no token and no link.
|
||||
if !strings.Contains(msg.HTML, "SMTP settings work") {
|
||||
t.Fatalf("Expected the email to state what it proves, got \n%v\n%v", msg.Subject, msg.HTML)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 204,
|
||||
ExpectedContent: []string{},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnMailerSend": 1,
|
||||
"OnMailerRecordVerificationSend": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorized as superuser (email change)",
|
||||
Method: http.MethodPost,
|
||||
URL: "/v1/settings/test/email",
|
||||
Body: strings.NewReader(`{
|
||||
"template": "email-change",
|
||||
"email": "[email protected]"
|
||||
}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
||||
},
|
||||
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||
if app.TestMailer.TotalSend() != 1 {
|
||||
t.Fatalf("[email-change] Expected 1 sent email, got %d", app.TestMailer.TotalSend())
|
||||
}
|
||||
|
||||
if len(app.TestMailer.LastMessage().To) != 1 {
|
||||
t.Fatalf("[email-change] Expected 1 recipient, got %v", app.TestMailer.LastMessage().To)
|
||||
}
|
||||
|
||||
if app.TestMailer.LastMessage().To[0].Address != "[email protected]" {
|
||||
t.Fatalf("[email-change] Expected the email to be sent to %s, got %s", "[email protected]", app.TestMailer.LastMessage().To[0].Address)
|
||||
}
|
||||
|
||||
if !strings.Contains(app.TestMailer.LastMessage().HTML, "Confirm new email") {
|
||||
t.Fatalf("[email-change] Expected to sent a confirm new email email, got \n%v\n%v", app.TestMailer.LastMessage().Subject, app.TestMailer.LastMessage().HTML)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 204,
|
||||
ExpectedContent: []string{},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnMailerSend": 1,
|
||||
"OnMailerRecordEmailChangeSend": 1,
|
||||
"*": 0,
|
||||
"OnMailerSend": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-18
@@ -1167,24 +1167,6 @@ type App interface {
|
||||
// It allows intercepting the email message or to use a custom mailer client.
|
||||
OnMailerSend() *hook.Hook[*MailerEvent]
|
||||
|
||||
// OnMailerBeforeRecordVerificationSend hook is triggered when
|
||||
// sending a verification email to an auth record, allowing
|
||||
// you to intercept and customize the email message that is being sent.
|
||||
//
|
||||
// If the optional "tags" list (Collection ids or names) is specified,
|
||||
// then all event handlers registered via the created hook will be
|
||||
// triggered and called only if their event data origin matches the tags.
|
||||
OnMailerRecordVerificationSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent]
|
||||
|
||||
// OnMailerRecordEmailChangeSend hook is triggered when sending a
|
||||
// confirmation new address email to an auth record, allowing
|
||||
// you to intercept and customize the email message that is being sent.
|
||||
//
|
||||
// If the optional "tags" list (Collection ids or names) is specified,
|
||||
// then all event handlers registered via the created hook will be
|
||||
// triggered and called only if their event data origin matches the tags.
|
||||
OnMailerRecordEmailChangeSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent]
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Realtime API event hooks
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@@ -165,8 +165,6 @@ type BaseApp struct {
|
||||
|
||||
// mailer event hooks
|
||||
onMailerSend *hook.Hook[*MailerEvent]
|
||||
onMailerRecordVerificationSend *hook.Hook[*MailerRecordEvent]
|
||||
onMailerRecordEmailChangeSend *hook.Hook[*MailerRecordEvent]
|
||||
|
||||
// realtime api event hooks
|
||||
onRealtimeConnectRequest *hook.Hook[*RealtimeConnectRequestEvent]
|
||||
@@ -348,8 +346,6 @@ func (app *BaseApp) initHooks() {
|
||||
|
||||
// mailer event hooks
|
||||
app.onMailerSend = &hook.Hook[*MailerEvent]{}
|
||||
app.onMailerRecordVerificationSend = &hook.Hook[*MailerRecordEvent]{}
|
||||
app.onMailerRecordEmailChangeSend = &hook.Hook[*MailerRecordEvent]{}
|
||||
|
||||
// realtime API event hooks
|
||||
app.onRealtimeConnectRequest = &hook.Hook[*RealtimeConnectRequestEvent]{}
|
||||
@@ -1113,14 +1109,6 @@ func (app *BaseApp) OnMailerSend() *hook.Hook[*MailerEvent] {
|
||||
return app.onMailerSend
|
||||
}
|
||||
|
||||
func (app *BaseApp) OnMailerRecordVerificationSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] {
|
||||
return hook.NewTaggedHook(app.onMailerRecordVerificationSend, tags...)
|
||||
}
|
||||
|
||||
func (app *BaseApp) OnMailerRecordEmailChangeSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] {
|
||||
return hook.NewTaggedHook(app.onMailerRecordEmailChangeSend, tags...)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Realtime API event hooks
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -45,8 +45,6 @@ func (m *Collection) unsetMissingOAuth2MappedFields() {
|
||||
|
||||
func (m *Collection) setDefaultAuthOptions() {
|
||||
m.collectionAuthOptions = collectionAuthOptions{
|
||||
VerificationTemplate: defaultVerificationTemplate,
|
||||
ConfirmEmailChangeTemplate: defaultConfirmEmailChangeTemplate,
|
||||
AuthRule: types.Pointer(""),
|
||||
AuthToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
@@ -112,8 +110,6 @@ type collectionAuthOptions struct {
|
||||
|
||||
// Default email templates
|
||||
// ---
|
||||
VerificationTemplate EmailTemplate `form:"verificationTemplate" json:"verificationTemplate"`
|
||||
ConfirmEmailChangeTemplate EmailTemplate `form:"confirmEmailChangeTemplate" json:"confirmEmailChangeTemplate"`
|
||||
}
|
||||
|
||||
func (o *collectionAuthOptions) validate(cv *collectionValidator) error {
|
||||
@@ -134,8 +130,6 @@ func (o *collectionAuthOptions) validate(cv *collectionValidator) error {
|
||||
validation.Field(&o.EmailChangeToken),
|
||||
validation.Field(&o.VerificationToken),
|
||||
validation.Field(&o.FileToken),
|
||||
validation.Field(&o.VerificationTemplate, validation.Required),
|
||||
validation.Field(&o.ConfirmEmailChangeTemplate, validation.Required),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -151,26 +151,6 @@ func TestCollectionAuthOptionsValidate(t *testing.T) {
|
||||
},
|
||||
expectedErrors: []string{"fileToken"},
|
||||
},
|
||||
|
||||
// templates
|
||||
{
|
||||
name: "trigger verificationTemplate validations",
|
||||
collection: func(app core.App) (*core.Collection, error) {
|
||||
c := core.NewAuthCollection("new_auth")
|
||||
c.VerificationTemplate.Body = ""
|
||||
return c, nil
|
||||
},
|
||||
expectedErrors: []string{"verificationTemplate"},
|
||||
},
|
||||
{
|
||||
name: "trigger confirmEmailChangeTemplate validations",
|
||||
collection: func(app core.App) (*core.Collection, error) {
|
||||
c := core.NewAuthCollection("new_auth")
|
||||
c.ConfirmEmailChangeTemplate.Body = ""
|
||||
return c, nil
|
||||
},
|
||||
expectedErrors: []string{"confirmEmailChangeTemplate"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
|
||||
@@ -658,7 +658,6 @@ func TestCollectionSerialize(t *testing.T) {
|
||||
`"type":"base"`,
|
||||
},
|
||||
[]string{
|
||||
"verificationTemplate",
|
||||
"manageRule",
|
||||
"authRule",
|
||||
"secret",
|
||||
@@ -687,7 +686,6 @@ func TestCollectionSerialize(t *testing.T) {
|
||||
`"viewQuery":"1=1"`,
|
||||
},
|
||||
[]string{
|
||||
"verificationTemplate",
|
||||
"manageRule",
|
||||
"authRule",
|
||||
"secret",
|
||||
@@ -783,7 +781,7 @@ func TestCollectionDBExport(t *testing.T) {
|
||||
},
|
||||
{
|
||||
core.CollectionTypeAuth,
|
||||
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"authRule":null,"manageRule":"1=6","oauth2":{"providers":null,"mappedFields":{"id":"","name":"","username":"","avatarURL":""},"enabled":false},"authToken":{"duration":0},"emailChangeToken":{"duration":0},"verificationToken":{"duration":0},"fileToken":{"duration":0},"verificationTemplate":{"subject":"","body":""},"confirmEmailChangeTemplate":{"subject":"","body":""}},"system":true,"type":"auth","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
|
||||
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"authRule":null,"manageRule":"1=6","oauth2":{"providers":null,"mappedFields":{"id":"","name":"","username":"","avatarURL":""},"enabled":false},"authToken":{"duration":0},"emailChangeToken":{"duration":0},"verificationToken":{"duration":0},"fileToken":{"duration":0}},"system":true,"type":"auth","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -163,12 +163,6 @@ type MailerEvent struct {
|
||||
Message *mailer.Message
|
||||
}
|
||||
|
||||
type MailerRecordEvent struct {
|
||||
MailerEvent
|
||||
baseRecordEventData
|
||||
Meta map[string]any
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Model events data
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
+21
-64
@@ -1,26 +1,27 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/mail"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/hanzoai/base/core"
|
||||
"github.com/hanzoai/base/mails"
|
||||
"github.com/hanzoai/base/tools/mailer"
|
||||
)
|
||||
|
||||
const (
|
||||
TestTemplateVerification = "verification"
|
||||
TestTemplateEmailChange = "email-change"
|
||||
)
|
||||
|
||||
// TestEmailSend is a email template test request form.
|
||||
// TestEmailSend proves the configured SMTP settings can deliver a message.
|
||||
//
|
||||
// It used to render one of the auth-record email templates against a throwaway
|
||||
// record, which made "can this deployment send mail" answerable only through a
|
||||
// flow this fork no longer has: verification and email-change were removed with
|
||||
// the rest of local auth, so both branches described pages that do not exist and
|
||||
// endpoints that answer 404. The remaining question is the one an operator
|
||||
// actually has after editing SMTP settings, and it needs no template, no
|
||||
// collection and no record — just a message that either arrives or does not.
|
||||
type TestEmailSend struct {
|
||||
app core.App
|
||||
|
||||
Email string `form:"email" json:"email"`
|
||||
Template string `form:"template" json:"template"`
|
||||
Collection string `form:"collection" json:"collection"` // optional, fallbacks to _superusers
|
||||
Email string `form:"email" json:"email"`
|
||||
}
|
||||
|
||||
// NewTestEmailSend creates and initializes new TestEmailSend form.
|
||||
@@ -31,74 +32,30 @@ func NewTestEmailSend(app core.App) *TestEmailSend {
|
||||
// Validate makes the form validatable by implementing [validation.Validatable] interface.
|
||||
func (form *TestEmailSend) Validate() error {
|
||||
return validation.ValidateStruct(form,
|
||||
validation.Field(
|
||||
&form.Collection,
|
||||
validation.Length(1, 255),
|
||||
validation.By(form.checkAuthCollection),
|
||||
),
|
||||
validation.Field(
|
||||
&form.Email,
|
||||
validation.Required,
|
||||
validation.Length(1, 255),
|
||||
is.EmailFormat,
|
||||
),
|
||||
validation.Field(
|
||||
&form.Template,
|
||||
validation.Required,
|
||||
validation.In(
|
||||
TestTemplateVerification,
|
||||
TestTemplateEmailChange,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (form *TestEmailSend) checkAuthCollection(value any) error {
|
||||
v, _ := value.(string)
|
||||
if v == "" {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
c, _ := form.app.FindCollectionByNameOrId(v)
|
||||
if c == nil || !c.IsAuth() {
|
||||
return validation.NewError("validation_invalid_auth_collection", "Must be a valid auth collection id or name.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Submit validates and sends a test email to the form.Email address.
|
||||
func (form *TestEmailSend) Submit() error {
|
||||
if err := form.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectionIdOrName := form.Collection
|
||||
if collectionIdOrName == "" {
|
||||
collectionIdOrName = core.CollectionNameSuperusers
|
||||
settings := form.app.Settings()
|
||||
name := settings.Meta.AppName
|
||||
|
||||
message := &mailer.Message{
|
||||
From: mail.Address{Name: settings.Meta.SenderName, Address: settings.Meta.SenderAddress},
|
||||
To: []mail.Address{{Address: form.Email}},
|
||||
Subject: name + " test email",
|
||||
HTML: "<p>This is a test message from " + name + ". Your SMTP settings work.</p>",
|
||||
}
|
||||
|
||||
collection, err := form.app.FindCollectionByNameOrId(collectionIdOrName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
for _, field := range collection.Fields {
|
||||
if field.GetHidden() {
|
||||
continue
|
||||
}
|
||||
record.Set(field.GetName(), "__test_"+field.GetName()+"__")
|
||||
}
|
||||
record.RefreshTokenKey()
|
||||
record.SetEmail(form.Email)
|
||||
|
||||
switch form.Template {
|
||||
case TestTemplateVerification:
|
||||
return mails.SendRecordVerification(form.app, record)
|
||||
case TestTemplateEmailChange:
|
||||
return mails.SendRecordChangeEmail(form.app, record, form.Email)
|
||||
default:
|
||||
return errors.New("unknown template " + form.Template)
|
||||
}
|
||||
return form.app.NewMailClient().Send(message)
|
||||
}
|
||||
|
||||
@@ -14,29 +14,22 @@ func TestEmailSendValidateAndSubmit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
template string
|
||||
email string
|
||||
collection string
|
||||
expectedErrors []string
|
||||
}{
|
||||
{"", "", "", []string{"template", "email"}},
|
||||
{"invalid", "[email protected]", "", []string{"template"}},
|
||||
{forms.TestTemplateVerification, "invalid", "", []string{"email"}},
|
||||
{forms.TestTemplateVerification, "[email protected]", "invalid", []string{"collection"}},
|
||||
{forms.TestTemplateVerification, "[email protected]", "demo1", []string{"collection"}},
|
||||
{forms.TestTemplateVerification, "[email protected]", "users", nil},
|
||||
{forms.TestTemplateEmailChange, "[email protected]", "", nil},
|
||||
{"", []string{"email"}},
|
||||
{"invalid", []string{"email"}},
|
||||
{strings.Repeat("a", 250) + "@example.com", []string{"email"}},
|
||||
{"[email protected]", nil},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%s", i, s.template), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%d_%s", i, s.email), func(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
form := forms.NewTestEmailSend(app)
|
||||
form.Email = s.email
|
||||
form.Template = s.template
|
||||
form.Collection = s.collection
|
||||
|
||||
result := form.Submit()
|
||||
|
||||
@@ -69,18 +62,15 @@ func TestEmailSendValidateAndSubmit(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
var expectedContent string
|
||||
switch s.template {
|
||||
case forms.TestTemplateEmailChange:
|
||||
expectedContent = "Confirm new email"
|
||||
case forms.TestTemplateVerification:
|
||||
expectedContent = "Verify"
|
||||
default:
|
||||
expectedContent = "__UNKNOWN_TEMPLATE__"
|
||||
// The point of the send is that SMTP works, so the message says so
|
||||
// and carries no token, no collection and no link — there is no flow
|
||||
// behind it to complete.
|
||||
msg := app.TestMailer.LastMessage()
|
||||
if !strings.Contains(msg.HTML, "SMTP settings work") {
|
||||
t.Errorf("Expected the email to state what it proves, got\n%v", msg.HTML)
|
||||
}
|
||||
|
||||
if !strings.Contains(app.TestMailer.LastMessage().HTML, expectedContent) {
|
||||
t.Errorf("Expected the email to contains %q, got\n%v", expectedContent, app.TestMailer.LastMessage().HTML)
|
||||
if len(msg.To) != 1 || msg.To[0].Address != s.email {
|
||||
t.Errorf("Expected the email addressed to %q, got %v", s.email, msg.To)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/hanzoai/base/apis"
|
||||
"github.com/hanzoai/base/core"
|
||||
"github.com/hanzoai/base/forms"
|
||||
"github.com/hanzoai/base/mails"
|
||||
"github.com/hanzoai/base/tools/filesystem"
|
||||
"github.com/hanzoai/base/tools/hook"
|
||||
"github.com/hanzoai/base/tools/inflector"
|
||||
@@ -676,14 +675,6 @@ func dbxBinds(vm *goja.Runtime) {
|
||||
obj.Set("notBetween", dbx.NotBetween)
|
||||
}
|
||||
|
||||
func mailsBinds(vm *goja.Runtime) {
|
||||
obj := vm.NewObject()
|
||||
vm.Set("$mails", obj)
|
||||
|
||||
obj.Set("sendRecordVerification", mails.SendRecordVerification)
|
||||
obj.Set("sendRecordChangeEmail", mails.SendRecordChangeEmail)
|
||||
}
|
||||
|
||||
func securityBinds(vm *goja.Runtime) {
|
||||
obj := vm.NewObject()
|
||||
vm.Set("$security", obj)
|
||||
|
||||
@@ -788,44 +788,6 @@ func TestDbxBinds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailsBindsCount(t *testing.T) {
|
||||
vm := goja.New()
|
||||
mailsBinds(vm)
|
||||
|
||||
testBindsCount(vm, "$mails", 2, t)
|
||||
}
|
||||
|
||||
func TestMailsBinds(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
record, err := app.FindAuthRecordByEmail("users", "[email protected]")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
mailsBinds(vm)
|
||||
vm.Set("$app", app)
|
||||
vm.Set("record", record)
|
||||
|
||||
_, vmErr := vm.RunString(`
|
||||
$mails.sendRecordVerification($app, record);
|
||||
if (!$app.testMailer.lastMessage().html.includes("/_/#/auth/confirm-verification/")) {
|
||||
throw new Error("Expected record verification email, got:" + JSON.stringify($app.testMailer.lastMessage()))
|
||||
}
|
||||
|
||||
$mails.sendRecordChangeEmail($app, record, "[email protected]");
|
||||
if (!$app.testMailer.lastMessage().html.includes("/_/#/auth/confirm-email-change/")) {
|
||||
throw new Error("Expected record email change email, got:" + JSON.stringify($app.testMailer.lastMessage()))
|
||||
}
|
||||
`)
|
||||
if vmErr != nil {
|
||||
t.Fatal(vmErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityBindsCount(t *testing.T) {
|
||||
vm := goja.New()
|
||||
securityBinds(vm)
|
||||
@@ -1594,7 +1556,7 @@ func TestHooksBindsCount(t *testing.T) {
|
||||
vm := goja.New()
|
||||
hooksBinds(app, vm, nil)
|
||||
|
||||
testBindsCount(vm, "this", 74, t)
|
||||
testBindsCount(vm, "this", 72, t)
|
||||
}
|
||||
|
||||
func TestHooksBinds(t *testing.T) {
|
||||
|
||||
+3845
-3970
File diff suppressed because it is too large
Load Diff
@@ -847,24 +847,6 @@ declare namespace $dbx {
|
||||
export let notBetween: dbx.notBetween
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// mailsBinds
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* ` + "`" + `$mails` + "`" + ` defines helpers to send common
|
||||
* auth records emails like verification, password reset, etc.
|
||||
*
|
||||
* @group Base
|
||||
*/
|
||||
declare namespace $mails {
|
||||
let sendRecordPasswordReset: mails.sendRecordPasswordReset
|
||||
let sendRecordVerification: mails.sendRecordVerification
|
||||
let sendRecordChangeEmail: mails.sendRecordChangeEmail
|
||||
let sendRecordOTP: mails.sendRecordOTP
|
||||
let sendRecordAuthAlert: mails.sendRecordAuthAlert
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// securityBinds
|
||||
// -------------------------------------------------------------------
|
||||
@@ -1276,7 +1258,6 @@ func main() {
|
||||
"github.com/hanzoai/base/tools/security": {"*"},
|
||||
"github.com/hanzoai/base/tools/filesystem": {"*"},
|
||||
"github.com/hanzoai/base/tools/template": {"*"},
|
||||
"github.com/hanzoai/base/mails": {"*"},
|
||||
"github.com/hanzoai/base/apis": {"*"},
|
||||
"github.com/hanzoai/base/core": {"*"},
|
||||
"github.com/hanzoai/base/forms": {"*"},
|
||||
|
||||
@@ -208,7 +208,6 @@ func (p *plugin) registerMigrations() error {
|
||||
httpClientBinds(vm)
|
||||
filesystemBinds(vm)
|
||||
formsBinds(vm)
|
||||
mailsBinds(vm)
|
||||
|
||||
vm.Set("$template", templateRegistry)
|
||||
vm.Set("__hooks", absHooksDir)
|
||||
@@ -297,7 +296,6 @@ func (p *plugin) registerHooks() error {
|
||||
httpClientBinds(vm)
|
||||
formsBinds(vm)
|
||||
apisBinds(vm)
|
||||
mailsBinds(vm)
|
||||
|
||||
vm.Set("$app", p.app)
|
||||
vm.Set("$template", templateRegistry)
|
||||
|
||||
@@ -31,10 +31,6 @@ migrate((app) => {
|
||||
"authToken": {
|
||||
"duration": 604800
|
||||
},
|
||||
"confirmEmailChangeTemplate": {
|
||||
"body": "<p>Hello,</p>\n<p>A new email address was requested for your {APP_NAME} account.</p>\n<p>Open your account to confirm it.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}\" target=\"_blank\" rel=\"noopener\">Open {APP_NAME}</a>\n</p>\n<p><i>If you didn't ask to change your email address, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
|
||||
"subject": "Confirm your {APP_NAME} new email address"
|
||||
},
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"emailChangeToken": {
|
||||
@@ -123,10 +119,6 @@ migrate((app) => {
|
||||
"system": true,
|
||||
"type": "auth",
|
||||
"updateRule": null,
|
||||
"verificationTemplate": {
|
||||
"body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Your email address is confirmed through your {APP_NAME} account.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}\" target=\"_blank\" rel=\"noopener\">Open {APP_NAME}</a>\n</p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
|
||||
"subject": "Verify your {APP_NAME} email"
|
||||
},
|
||||
"verificationToken": {
|
||||
"duration": 259200
|
||||
},
|
||||
@@ -160,10 +152,6 @@ func init() {
|
||||
"authToken": {
|
||||
"duration": 604800
|
||||
},
|
||||
"confirmEmailChangeTemplate": {
|
||||
"body": "<p>Hello,</p>\n<p>A new email address was requested for your {APP_NAME} account.</p>\n<p>Open your account to confirm it.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}\" target=\"_blank\" rel=\"noopener\">Open {APP_NAME}</a>\n</p>\n<p><i>If you didn't ask to change your email address, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
|
||||
"subject": "Confirm your {APP_NAME} new email address"
|
||||
},
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"emailChangeToken": {
|
||||
@@ -252,10 +240,6 @@ func init() {
|
||||
"system": true,
|
||||
"type": "auth",
|
||||
"updateRule": null,
|
||||
"verificationTemplate": {
|
||||
"body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Your email address is confirmed through your {APP_NAME} account.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}\" target=\"_blank\" rel=\"noopener\">Open {APP_NAME}</a>\n</p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
|
||||
"subject": "Verify your {APP_NAME} email"
|
||||
},
|
||||
"verificationToken": {
|
||||
"duration": 259200
|
||||
},
|
||||
@@ -377,10 +361,6 @@ migrate((app) => {
|
||||
"authToken": {
|
||||
"duration": 604800
|
||||
},
|
||||
"confirmEmailChangeTemplate": {
|
||||
"body": "<p>Hello,</p>\n<p>A new email address was requested for your {APP_NAME} account.</p>\n<p>Open your account to confirm it.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}\" target=\"_blank\" rel=\"noopener\">Open {APP_NAME}</a>\n</p>\n<p><i>If you didn't ask to change your email address, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
|
||||
"subject": "Confirm your {APP_NAME} new email address"
|
||||
},
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"emailChangeToken": {
|
||||
@@ -469,10 +449,6 @@ migrate((app) => {
|
||||
"system": false,
|
||||
"type": "auth",
|
||||
"updateRule": null,
|
||||
"verificationTemplate": {
|
||||
"body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Your email address is confirmed through your {APP_NAME} account.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}\" target=\"_blank\" rel=\"noopener\">Open {APP_NAME}</a>\n</p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
|
||||
"subject": "Verify your {APP_NAME} email"
|
||||
},
|
||||
"verificationToken": {
|
||||
"duration": 259200
|
||||
},
|
||||
@@ -509,10 +485,6 @@ func init() {
|
||||
"authToken": {
|
||||
"duration": 604800
|
||||
},
|
||||
"confirmEmailChangeTemplate": {
|
||||
"body": "<p>Hello,</p>\n<p>A new email address was requested for your {APP_NAME} account.</p>\n<p>Open your account to confirm it.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}\" target=\"_blank\" rel=\"noopener\">Open {APP_NAME}</a>\n</p>\n<p><i>If you didn't ask to change your email address, you can ignore this email.</i></p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
|
||||
"subject": "Confirm your {APP_NAME} new email address"
|
||||
},
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"emailChangeToken": {
|
||||
@@ -601,10 +573,6 @@ func init() {
|
||||
"system": false,
|
||||
"type": "auth",
|
||||
"updateRule": null,
|
||||
"verificationTemplate": {
|
||||
"body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Your email address is confirmed through your {APP_NAME} account.</p>\n<p>\n <a class=\"btn\" href=\"{APP_URL}\" target=\"_blank\" rel=\"noopener\">Open {APP_NAME}</a>\n</p>\n<p>\n Thanks,<br/>\n {APP_NAME} team\n</p>",
|
||||
"subject": "Verify your {APP_NAME} email"
|
||||
},
|
||||
"verificationToken": {
|
||||
"duration": 259200
|
||||
},
|
||||
|
||||
@@ -509,22 +509,6 @@ func NewTestAppWithConfig(config core.BaseAppConfig) (*TestApp, error) {
|
||||
Priority: -99999,
|
||||
})
|
||||
|
||||
t.OnMailerRecordVerificationSend().Bind(&hook.Handler[*core.MailerRecordEvent]{
|
||||
Func: func(e *core.MailerRecordEvent) error {
|
||||
t.registerEventCall("OnMailerRecordVerificationSend")
|
||||
return e.Next()
|
||||
},
|
||||
Priority: -99999,
|
||||
})
|
||||
|
||||
t.OnMailerRecordEmailChangeSend().Bind(&hook.Handler[*core.MailerRecordEvent]{
|
||||
Func: func(e *core.MailerRecordEvent) error {
|
||||
t.registerEventCall("OnMailerRecordEmailChangeSend")
|
||||
return e.Next()
|
||||
},
|
||||
Priority: -99999,
|
||||
})
|
||||
|
||||
t.OnRealtimeConnectRequest().Bind(&hook.Handler[*core.RealtimeConnectRequestEvent]{
|
||||
Func: func(e *core.RealtimeConnectRequestEvent) error {
|
||||
t.registerEventCall("OnRealtimeConnectRequest")
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>Base Admin</title>
|
||||
<script type="module" crossorigin src="/assets/index-D7VD1BqT.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CwoAdfZi.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Clip6ok4.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -6,7 +6,6 @@ const navItems = [
|
||||
{ to: '/settings/smtp', label: 'SMTP' },
|
||||
{ to: '/settings/backups', label: 'Backups' },
|
||||
{ to: '/settings/auth', label: 'Auth providers' },
|
||||
{ to: '/settings/mail', label: 'Mail templates' },
|
||||
{ to: '/settings/tokens', label: 'Token options' },
|
||||
{ to: '/settings/data', label: 'Import / Export' },
|
||||
{ to: '/settings/superusers', label: 'Superusers' },
|
||||
|
||||
Reference in New Issue
Block a user