diff --git a/internal/start/start.go b/internal/start/start.go index b19a54168..bdb0989aa 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -552,10 +552,10 @@ EOF id+filepath.Ext(tmpl.ContentPath), )) } - if len(tmpl.Subject) > 0 { + if tmpl.Subject != nil { env = append(env, fmt.Sprintf("GOTRUE_MAILER_SUBJECTS_%s=%s", strings.ToUpper(id), - tmpl.Subject, + *tmpl.Subject, )) } } diff --git a/pkg/config/api_test.go b/pkg/config/api_test.go index d48001244..92859c671 100644 --- a/pkg/config/api_test.go +++ b/pkg/config/api_test.go @@ -33,7 +33,7 @@ func TestApiToUpdatePostgrestConfigBody(t *testing.T) { }) } -func TestApiDiffWithRemote(t *testing.T) { +func TestApiDiff(t *testing.T) { t.Run("detects differences", func(t *testing.T) { api := &api{ Enabled: true, @@ -49,14 +49,9 @@ func TestApiDiffWithRemote(t *testing.T) { } diff, err := api.DiffWithRemote(remoteConfig) - assert.NoError(t, err, string(diff)) - - assert.Contains(t, string(diff), "-schemas = [\"public\"]") - assert.Contains(t, string(diff), "+schemas = [\"public\", \"private\"]") - assert.Contains(t, string(diff), "-extra_search_path = [\"public\"]") - assert.Contains(t, string(diff), "+extra_search_path = [\"extensions\", \"public\"]") - assert.Contains(t, string(diff), "-max_rows = 500") - assert.Contains(t, string(diff), "+max_rows = 1000") + assert.NoError(t, err) + + assertSnapshotEqual(t, diff) }) t.Run("handles no differences", func(t *testing.T) { @@ -114,10 +109,9 @@ func TestApiDiffWithRemote(t *testing.T) { } diff, err := api.DiffWithRemote(remoteConfig) - assert.NoError(t, err, string(diff)) + assert.NoError(t, err) - assert.Contains(t, string(diff), "-enabled = false") - assert.Contains(t, string(diff), "+enabled = true") + assertSnapshotEqual(t, diff) }) t.Run("handles api disabled on local side", func(t *testing.T) { @@ -135,9 +129,8 @@ func TestApiDiffWithRemote(t *testing.T) { } diff, err := api.DiffWithRemote(remoteConfig) - assert.NoError(t, err, string(diff)) + assert.NoError(t, err) - assert.Contains(t, string(diff), "-enabled = true") - assert.Contains(t, string(diff), "+enabled = false") + assertSnapshotEqual(t, diff) }) } diff --git a/pkg/config/auth.go b/pkg/config/auth.go index 4ac13d84b..a2ea016f2 100644 --- a/pkg/config/auth.go +++ b/pkg/config/auth.go @@ -89,7 +89,9 @@ type ( } emailTemplate struct { - Subject string `toml:"subject"` + Subject *string `toml:"subject"` + Content *string `toml:"content"` + // Only content path is accepted in config.toml ContentPath string `toml:"content_path"` } @@ -326,6 +328,28 @@ func (e email) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { // Setting a single empty string disables SMTP body.SmtpHost = cast.Ptr("") } + if len(e.Template) == 0 { + return + } + var tmpl *emailTemplate + tmpl = cast.Ptr(e.Template["invite"]) + body.MailerSubjectsInvite = tmpl.Subject + body.MailerTemplatesInviteContent = tmpl.Content + tmpl = cast.Ptr(e.Template["confirmation"]) + body.MailerSubjectsConfirmation = tmpl.Subject + body.MailerTemplatesConfirmationContent = tmpl.Content + tmpl = cast.Ptr(e.Template["recovery"]) + body.MailerSubjectsRecovery = tmpl.Subject + body.MailerTemplatesRecoveryContent = tmpl.Content + tmpl = cast.Ptr(e.Template["magic_link"]) + body.MailerSubjectsMagicLink = tmpl.Subject + body.MailerTemplatesMagicLinkContent = tmpl.Content + tmpl = cast.Ptr(e.Template["email_change"]) + body.MailerSubjectsEmailChange = tmpl.Subject + body.MailerTemplatesEmailChangeContent = tmpl.Content + tmpl = cast.Ptr(e.Template["reauthentication"]) + body.MailerSubjectsReauthentication = tmpl.Subject + body.MailerTemplatesReauthenticationContent = tmpl.Content } func (e *email) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) { @@ -352,6 +376,64 @@ func (e *email) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) { } else { e.Smtp = nil } + if len(e.Template) == 0 { + return + } + var tmpl emailTemplate + // When local config is not set, we assume platform defaults should not change + tmpl = e.Template["invite"] + if tmpl.Subject != nil { + tmpl.Subject = remoteConfig.MailerSubjectsInvite + } + if tmpl.Content != nil { + tmpl.Content = remoteConfig.MailerTemplatesInviteContent + } + e.Template["invite"] = tmpl + + tmpl = e.Template["confirmation"] + if tmpl.Subject != nil { + tmpl.Subject = remoteConfig.MailerSubjectsConfirmation + } + if tmpl.Content != nil { + tmpl.Content = remoteConfig.MailerTemplatesConfirmationContent + } + e.Template["confirmation"] = tmpl + + tmpl = e.Template["recovery"] + if tmpl.Subject != nil { + tmpl.Subject = remoteConfig.MailerSubjectsRecovery + } + if tmpl.Content != nil { + tmpl.Content = remoteConfig.MailerTemplatesRecoveryContent + } + e.Template["recovery"] = tmpl + + tmpl = e.Template["magic_link"] + if tmpl.Subject != nil { + tmpl.Subject = remoteConfig.MailerSubjectsMagicLink + } + if tmpl.Content != nil { + tmpl.Content = remoteConfig.MailerTemplatesMagicLinkContent + } + e.Template["magic_link"] = tmpl + + tmpl = e.Template["email_change"] + if tmpl.Subject != nil { + tmpl.Subject = remoteConfig.MailerSubjectsEmailChange + } + if tmpl.Content != nil { + tmpl.Content = remoteConfig.MailerTemplatesEmailChangeContent + } + e.Template["email_change"] = tmpl + + tmpl = e.Template["reauthentication"] + if tmpl.Subject != nil { + tmpl.Subject = remoteConfig.MailerSubjectsReauthentication + } + if tmpl.Content != nil { + tmpl.Content = remoteConfig.MailerTemplatesReauthenticationContent + } + e.Template["reauthentication"] = tmpl } func (s sms) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { diff --git a/pkg/config/auth_test.go b/pkg/config/auth_test.go index 7f5c4d20f..e8d373b9f 100644 --- a/pkg/config/auth_test.go +++ b/pkg/config/auth_test.go @@ -1,9 +1,12 @@ package config import ( + "os" + "path/filepath" "testing" "time" + "github.com/go-errors/errors" "github.com/stretchr/testify/assert" v1API "github.com/supabase/cli/pkg/api" "github.com/supabase/cli/pkg/cast" @@ -18,6 +21,16 @@ func newWithDefaults() auth { } } +func assertSnapshotEqual(t *testing.T, actual []byte) { + snapshot := filepath.Join("testdata", filepath.FromSlash(t.Name())) + ".diff" + expected, err := os.ReadFile(snapshot) + if errors.Is(err, os.ErrNotExist) { + assert.NoError(t, os.MkdirAll(filepath.Dir(snapshot), 0755)) + assert.NoError(t, os.WriteFile(snapshot, actual, 0600)) + } + assert.Equal(t, string(expected), string(actual)) +} + func TestHookDiff(t *testing.T) { t.Run("local and remote enabled", func(t *testing.T) { c := newWithDefaults() @@ -68,18 +81,7 @@ func TestHookDiff(t *testing.T) { }) // Check error assert.NoError(t, err) - - assert.Contains(t, string(diff), ` [hook.mfa_verification_attempt]`) - assert.Contains(t, string(diff), `-enabled = true`) - assert.Contains(t, string(diff), `+enabled = false`) - assert.Contains(t, string(diff), ` uri = ""`) - assert.Contains(t, string(diff), ` secrets = ""`) - - assert.Contains(t, string(diff), ` [hook.custom_access_token]`) - assert.Contains(t, string(diff), `-enabled = false`) - assert.Contains(t, string(diff), `+enabled = true`) - assert.Contains(t, string(diff), ` uri = ""`) - assert.Contains(t, string(diff), ` secrets = "hash:b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"`) + assertSnapshotEqual(t, diff) }) t.Run("local and remote disabled", func(t *testing.T) { @@ -168,26 +170,7 @@ func TestMfaDiff(t *testing.T) { }) // Check error assert.NoError(t, err) - assert.Contains(t, string(diff), ` [mfa]`) - assert.Contains(t, string(diff), `-max_enrolled_factors = 10`) - assert.Contains(t, string(diff), `+max_enrolled_factors = 0`) - assert.Contains(t, string(diff), ` [mfa.totp]`) - assert.Contains(t, string(diff), ` enroll_enabled = false`) - assert.Contains(t, string(diff), ` verify_enabled = false`) - assert.Contains(t, string(diff), ` [mfa.phone]`) - assert.Contains(t, string(diff), `-enroll_enabled = false`) - assert.Contains(t, string(diff), `-verify_enabled = false`) - assert.Contains(t, string(diff), `-otp_length = 6`) - assert.Contains(t, string(diff), `-template = "Your code is {{ .Code }}"`) - assert.Contains(t, string(diff), `-max_frequency = "5s"`) - assert.Contains(t, string(diff), `+enroll_enabled = true`) - assert.Contains(t, string(diff), `+verify_enabled = true`) - assert.Contains(t, string(diff), `+otp_length = 0`) - assert.Contains(t, string(diff), `+template = ""`) - assert.Contains(t, string(diff), `+max_frequency = "0s"`) - assert.Contains(t, string(diff), ` [mfa.web_authn]`) - assert.Contains(t, string(diff), ` enroll_enabled = false`) - assert.Contains(t, string(diff), ` verify_enabled = false`) + assertSnapshotEqual(t, diff) }) t.Run("local and remote disabled", func(t *testing.T) { @@ -228,11 +211,30 @@ func TestEmailDiff(t *testing.T) { EnableConfirmations: true, SecurePasswordChange: true, Template: map[string]emailTemplate{ - "invite": {}, - "confirmation": {}, - "recovery": {}, - "magic_link": {}, - "email_change": {}, + "invite": { + Subject: cast.Ptr("invite-subject"), + Content: cast.Ptr("invite-content"), + }, + "confirmation": { + Subject: cast.Ptr("confirmation-subject"), + Content: cast.Ptr("confirmation-content"), + }, + "recovery": { + Subject: cast.Ptr("recovery-subject"), + Content: cast.Ptr("recovery-content"), + }, + "magic_link": { + Subject: cast.Ptr("magic-link-subject"), + Content: cast.Ptr("magic-link-content"), + }, + "email_change": { + Subject: cast.Ptr("email-change-subject"), + Content: cast.Ptr("email-change-content"), + }, + "reauthentication": { + Subject: cast.Ptr("reauthentication-subject"), + Content: cast.Ptr("reauthentication-content"), + }, }, Smtp: &smtp{ Host: "smtp.sendgrid.net", @@ -261,6 +263,19 @@ func TestEmailDiff(t *testing.T) { SmtpAdminEmail: cast.Ptr("admin@email.com"), SmtpSenderName: cast.Ptr("Admin"), SmtpMaxFrequency: cast.Ptr(1), + // Custom templates + MailerSubjectsInvite: cast.Ptr("invite-subject"), + MailerTemplatesInviteContent: cast.Ptr("invite-content"), + MailerSubjectsConfirmation: cast.Ptr("confirmation-subject"), + MailerTemplatesConfirmationContent: cast.Ptr("confirmation-content"), + MailerSubjectsRecovery: cast.Ptr("recovery-subject"), + MailerTemplatesRecoveryContent: cast.Ptr("recovery-content"), + MailerSubjectsMagicLink: cast.Ptr("magic-link-subject"), + MailerTemplatesMagicLinkContent: cast.Ptr("magic-link-content"), + MailerSubjectsEmailChange: cast.Ptr("email-change-subject"), + MailerTemplatesEmailChangeContent: cast.Ptr("email-change-content"), + MailerSubjectsReauthentication: cast.Ptr("reauthentication-subject"), + MailerTemplatesReauthenticationContent: cast.Ptr("reauthentication-content"), }) // Check error assert.NoError(t, err) @@ -275,11 +290,28 @@ func TestEmailDiff(t *testing.T) { EnableConfirmations: true, SecurePasswordChange: true, Template: map[string]emailTemplate{ - "invite": {}, - "confirmation": {}, - "recovery": {}, - "magic_link": {}, - "email_change": {}, + "invite": { + Subject: cast.Ptr("invite-subject"), + Content: cast.Ptr("invite-content"), + }, + "confirmation": { + Subject: cast.Ptr("confirmation-subject"), + }, + "recovery": { + Content: cast.Ptr("recovery-content"), + }, + "magic_link": { + Subject: cast.Ptr("magic-link-subject"), + Content: cast.Ptr("magic-link-content"), + }, + "email_change": { + Subject: cast.Ptr("email-change-subject"), + Content: cast.Ptr("email-change-content"), + }, + "reauthentication": { + Subject: cast.Ptr(""), + Content: cast.Ptr(""), + }, }, Smtp: &smtp{ Host: "smtp.sendgrid.net", @@ -302,24 +334,15 @@ func TestEmailDiff(t *testing.T) { MailerOtpExp: 3600, SecurityUpdatePasswordRequireReauthentication: cast.Ptr(false), SmtpMaxFrequency: cast.Ptr(60), + // Custom templates + MailerTemplatesConfirmationContent: cast.Ptr("confirmation-content"), + MailerSubjectsRecovery: cast.Ptr("recovery-subject"), + MailerSubjectsMagicLink: cast.Ptr("magic-link-subject"), + MailerTemplatesEmailChangeContent: cast.Ptr("email-change-content"), }) // Check error assert.NoError(t, err) - assert.Contains(t, string(diff), ` [email]`) - assert.Contains(t, string(diff), `-enable_signup = false`) - assert.Contains(t, string(diff), `-double_confirm_changes = false`) - assert.Contains(t, string(diff), `-enable_confirmations = false`) - assert.Contains(t, string(diff), `-secure_password_change = false`) - assert.Contains(t, string(diff), `-max_frequency = "1m0s"`) - assert.Contains(t, string(diff), `-otp_length = 6`) - assert.Contains(t, string(diff), `-otp_expiry = 3600`) - assert.Contains(t, string(diff), `+enable_signup = true`) - assert.Contains(t, string(diff), `+double_confirm_changes = true`) - assert.Contains(t, string(diff), `+enable_confirmations = true`) - assert.Contains(t, string(diff), `+secure_password_change = true`) - assert.Contains(t, string(diff), `+max_frequency = "1s"`) - assert.Contains(t, string(diff), `+otp_length = 8`) - assert.Contains(t, string(diff), `+otp_expiry = 86400`) + assertSnapshotEqual(t, diff) }) t.Run("local disabled remote enabled", func(t *testing.T) { @@ -327,11 +350,12 @@ func TestEmailDiff(t *testing.T) { c.Email = email{ EnableConfirmations: false, Template: map[string]emailTemplate{ - "invite": {}, - "confirmation": {}, - "recovery": {}, - "magic_link": {}, - "email_change": {}, + "invite": {}, + "confirmation": {}, + "recovery": {}, + "magic_link": {}, + "email_change": {}, + "reauthentication": {}, }, MaxFrequency: time.Minute, OtpLength: 8, @@ -352,33 +376,23 @@ func TestEmailDiff(t *testing.T) { SmtpAdminEmail: cast.Ptr("admin@email.com"), SmtpSenderName: cast.Ptr("Admin"), SmtpMaxFrequency: cast.Ptr(1), + // Custom templates + MailerSubjectsInvite: cast.Ptr("invite-subject"), + MailerTemplatesInviteContent: cast.Ptr("invite-content"), + MailerSubjectsConfirmation: cast.Ptr("confirmation-subject"), + MailerTemplatesConfirmationContent: cast.Ptr("confirmation-content"), + MailerSubjectsRecovery: cast.Ptr("recovery-subject"), + MailerTemplatesRecoveryContent: cast.Ptr("recovery-content"), + MailerSubjectsMagicLink: cast.Ptr("magic-link-subject"), + MailerTemplatesMagicLinkContent: cast.Ptr("magic-link-content"), + MailerSubjectsEmailChange: cast.Ptr("email-change-subject"), + MailerTemplatesEmailChangeContent: cast.Ptr("email-change-content"), + MailerSubjectsReauthentication: cast.Ptr("reauthentication-subject"), + MailerTemplatesReauthenticationContent: cast.Ptr("reauthentication-content"), }) // Check error assert.NoError(t, err) - - assert.Contains(t, string(diff), ` [email]`) - assert.Contains(t, string(diff), `-enable_signup = true`) - assert.Contains(t, string(diff), `-double_confirm_changes = true`) - assert.Contains(t, string(diff), `-enable_confirmations = true`) - assert.Contains(t, string(diff), `-secure_password_change = true`) - assert.Contains(t, string(diff), `-max_frequency = "1s"`) - assert.Contains(t, string(diff), `-otp_length = 6`) - assert.Contains(t, string(diff), `-otp_expiry = 3600`) - assert.Contains(t, string(diff), `+enable_signup = false`) - assert.Contains(t, string(diff), `+double_confirm_changes = false`) - assert.Contains(t, string(diff), `+enable_confirmations = false`) - assert.Contains(t, string(diff), `+secure_password_change = false`) - assert.Contains(t, string(diff), `+max_frequency = "1m0s"`) - assert.Contains(t, string(diff), `+otp_length = 8`) - assert.Contains(t, string(diff), `+otp_expiry = 86400`) - - assert.Contains(t, string(diff), `-[email.smtp]`) - assert.Contains(t, string(diff), `-host = "smtp.sendgrid.net"`) - assert.Contains(t, string(diff), `-port = 587`) - assert.Contains(t, string(diff), `-user = "apikey"`) - assert.Contains(t, string(diff), `-pass = "hash:ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e"`) - assert.Contains(t, string(diff), `-admin_email = "admin@email.com"`) - assert.Contains(t, string(diff), `-sender_name = "Admin"`) + assertSnapshotEqual(t, diff) }) t.Run("local disabled remote disabled", func(t *testing.T) { @@ -386,11 +400,12 @@ func TestEmailDiff(t *testing.T) { c.Email = email{ EnableConfirmations: false, Template: map[string]emailTemplate{ - "invite": {}, - "confirmation": {}, - "recovery": {}, - "magic_link": {}, - "email_change": {}, + "invite": {}, + "confirmation": {}, + "recovery": {}, + "magic_link": {}, + "email_change": {}, + "reauthentication": {}, }, MaxFrequency: time.Minute, OtpLength: 6, @@ -480,23 +495,7 @@ func TestSmsDiff(t *testing.T) { }) // Check error assert.NoError(t, err) - assert.Contains(t, string(diff), `-enable_signup = true`) - assert.Contains(t, string(diff), `-enable_confirmations = true`) - assert.Contains(t, string(diff), `-template = "Your code is {{ .Code }}"`) - assert.Contains(t, string(diff), `-max_frequency = "1m0s"`) - - assert.Contains(t, string(diff), `+enable_signup = false`) - assert.Contains(t, string(diff), `+enable_confirmations = false`) - assert.Contains(t, string(diff), `+template = ""`) - assert.Contains(t, string(diff), `+max_frequency = "0s"`) - - assert.Contains(t, string(diff), ` [sms.twilio]`) - assert.Contains(t, string(diff), `-enabled = true`) - assert.Contains(t, string(diff), `+enabled = false`) - - assert.Contains(t, string(diff), `-[sms.test_otp]`) - assert.Contains(t, string(diff), `-123 = "456"`) - assert.Contains(t, string(diff), `-456 = "123"`) + assertSnapshotEqual(t, diff) }) t.Run("local enabled remote disabled", func(t *testing.T) { @@ -529,30 +528,7 @@ func TestSmsDiff(t *testing.T) { }) // Check error assert.NoError(t, err) - assert.Contains(t, string(diff), `-enable_signup = false`) - assert.Contains(t, string(diff), `-enable_confirmations = false`) - assert.Contains(t, string(diff), `-template = ""`) - assert.Contains(t, string(diff), `-max_frequency = "0s"`) - - assert.Contains(t, string(diff), `+enable_signup = true`) - assert.Contains(t, string(diff), `+enable_confirmations = true`) - assert.Contains(t, string(diff), `+template = "Your code is {{ .Code }}"`) - assert.Contains(t, string(diff), `+max_frequency = "1m0s"`) - - assert.Contains(t, string(diff), ` [sms.twilio]`) - assert.Contains(t, string(diff), `-enabled = true`) - assert.Contains(t, string(diff), `+enabled = false`) - - assert.Contains(t, string(diff), ` [sms.messagebird]`) - assert.Contains(t, string(diff), `-enabled = false`) - assert.Contains(t, string(diff), `-originator = ""`) - assert.Contains(t, string(diff), `-access_key = "hash:"`) - assert.Contains(t, string(diff), `+enabled = true`) - assert.Contains(t, string(diff), `+originator = "test-originator"`) - assert.Contains(t, string(diff), `+access_key = "hash:ab60d03fc809fb02dae838582f3ddc13d1d6cb32ffba77c4b969dd3caa496f13"`) - - assert.Contains(t, string(diff), `+[sms.test_otp]`) - assert.Contains(t, string(diff), `+123 = "456"`) + assertSnapshotEqual(t, diff) }) t.Run("local disabled remote disabled", func(t *testing.T) { @@ -597,9 +573,7 @@ func TestSmsDiff(t *testing.T) { }) // Check error assert.NoError(t, err) - assert.Contains(t, string(diff), ` [sms]`) - assert.Contains(t, string(diff), `-enable_signup = false`) - assert.Contains(t, string(diff), `+enable_signup = true`) + assertSnapshotEqual(t, diff) }) t.Run("enable provider without sign up", func(t *testing.T) { @@ -762,15 +736,7 @@ func TestExternalDiff(t *testing.T) { }) // Check error assert.NoError(t, err) - assert.Contains(t, string(diff), ` [external.apple]`) - assert.Contains(t, string(diff), `-enabled = false`) - assert.Contains(t, string(diff), `+enabled = true`) - assert.Contains(t, string(diff), ` client_id = "test-client-1,test-client-2"`) - assert.Contains(t, string(diff), ` secret = "hash:ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"`) - - assert.Contains(t, string(diff), ` [external.google]`) - assert.Contains(t, string(diff), `-enabled = true`) - assert.Contains(t, string(diff), `+enabled = false`) + assertSnapshotEqual(t, diff) }) t.Run("local and remote disabled", func(t *testing.T) { diff --git a/pkg/config/config.go b/pkg/config/config.go index 34e336475..b1e7ed570 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -30,6 +30,7 @@ import ( "github.com/joho/godotenv" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" + "github.com/supabase/cli/pkg/cast" "github.com/supabase/cli/pkg/fetcher" "golang.org/x/mod/semver" ) @@ -292,11 +293,12 @@ func NewConfig(editors ...ConfigEditor) config { Image: gotrueImage, Email: email{ Template: map[string]emailTemplate{ - "invite": {}, - "confirmation": {}, - "recovery": {}, - "magic_link": {}, - "email_change": {}, + "invite": {}, + "confirmation": {}, + "recovery": {}, + "magic_link": {}, + "email_change": {}, + "reauthentication": {}, }, }, External: map[string]provider{ @@ -817,11 +819,18 @@ func (c *seed) loadSeedPaths(basePath string, fsys fs.FS) error { func (e *email) validate(fsys fs.FS) (err error) { for name, tmpl := range e.Template { - if len(tmpl.ContentPath) > 0 { - if _, err = fs.Stat(fsys, filepath.Clean(tmpl.ContentPath)); err != nil { - return errors.Errorf("Invalid config for auth.email.%s.content_path: %s", name, tmpl.ContentPath) + if len(tmpl.ContentPath) == 0 { + if tmpl.Content != nil { + return errors.Errorf("Invalid config for auth.email.%s.content: please use content_path instead", name) } + continue + } + if content, err := fs.ReadFile(fsys, filepath.Clean(tmpl.ContentPath)); err != nil { + return errors.Errorf("Invalid config for auth.email.%s.content_path: %w", name, err) + } else { + tmpl.Content = cast.Ptr(string(content)) } + e.Template[name] = tmpl } if e.Smtp != nil { if len(e.Smtp.Host) == 0 { diff --git a/pkg/config/db_test.go b/pkg/config/db_test.go index 8d70ec21b..575fd202d 100644 --- a/pkg/config/db_test.go +++ b/pkg/config/db_test.go @@ -42,7 +42,7 @@ func TestDbSettingsToUpdatePostgresConfigBody(t *testing.T) { }) } -func TestDbSettingsDiffWithRemote(t *testing.T) { +func TestDbSettingsDiff(t *testing.T) { t.Run("detects differences", func(t *testing.T) { db := &db{ Settings: settings{ @@ -61,12 +61,7 @@ func TestDbSettingsDiffWithRemote(t *testing.T) { diff, err := db.Settings.DiffWithRemote(remoteConfig) assert.NoError(t, err) - assert.Contains(t, string(diff), "-effective_cache_size = \"8GB\"") - assert.Contains(t, string(diff), "+effective_cache_size = \"4GB\"") - assert.Contains(t, string(diff), "-max_connections = 200") - assert.Contains(t, string(diff), "+max_connections = 100") - assert.Contains(t, string(diff), "-shared_buffers = \"2GB\"") - assert.Contains(t, string(diff), "+shared_buffers = \"1GB\"") + assertSnapshotEqual(t, diff) }) t.Run("handles no differences", func(t *testing.T) { @@ -127,9 +122,7 @@ func TestDbSettingsDiffWithRemote(t *testing.T) { diff, err := db.Settings.DiffWithRemote(remoteConfig) assert.NoError(t, err) - assert.Contains(t, string(diff), "+effective_cache_size = \"4GB\"") - assert.Contains(t, string(diff), "+max_connections = 100") - assert.Contains(t, string(diff), "+shared_buffers = \"1GB\"") + assertSnapshotEqual(t, diff) }) t.Run("handles api disabled on local side", func(t *testing.T) { @@ -148,9 +141,7 @@ func TestDbSettingsDiffWithRemote(t *testing.T) { diff, err := db.Settings.DiffWithRemote(remoteConfig) assert.NoError(t, err) - assert.Contains(t, string(diff), "-effective_cache_size = \"4GB\"") - assert.Contains(t, string(diff), "-max_connections = 100") - assert.Contains(t, string(diff), "-shared_buffers = \"1GB\"") + assertSnapshotEqual(t, diff) }) } diff --git a/pkg/config/testdata/TestApiDiff/detects_differences.diff b/pkg/config/testdata/TestApiDiff/detects_differences.diff new file mode 100644 index 000000000..5d0e9603e --- /dev/null +++ b/pkg/config/testdata/TestApiDiff/detects_differences.diff @@ -0,0 +1,14 @@ +diff remote[api] local[api] +--- remote[api] ++++ local[api] +@@ -1,7 +1,7 @@ + enabled = true +-schemas = ["public"] +-extra_search_path = ["public"] +-max_rows = 500 ++schemas = ["public", "private"] ++extra_search_path = ["extensions", "public"] ++max_rows = 1000 + port = 0 + external_url = "" + diff --git a/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_local_side.diff b/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_local_side.diff new file mode 100644 index 000000000..67a5ccd5c --- /dev/null +++ b/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_local_side.diff @@ -0,0 +1,9 @@ +diff remote[api] local[api] +--- remote[api] ++++ local[api] +@@ -1,4 +1,4 @@ +-enabled = true ++enabled = false + schemas = ["public"] + extra_search_path = ["public"] + max_rows = 500 diff --git a/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_remote_side.diff b/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_remote_side.diff new file mode 100644 index 000000000..200d36c40 --- /dev/null +++ b/pkg/config/testdata/TestApiDiff/handles_api_disabled_on_remote_side.diff @@ -0,0 +1,9 @@ +diff remote[api] local[api] +--- remote[api] ++++ local[api] +@@ -1,4 +1,4 @@ +-enabled = false ++enabled = true + schemas = ["public", "private"] + extra_search_path = ["extensions", "public"] + max_rows = 500 diff --git a/pkg/config/testdata/TestDbSettingsDiff/detects_differences.diff b/pkg/config/testdata/TestDbSettingsDiff/detects_differences.diff new file mode 100644 index 000000000..12aeb50ae --- /dev/null +++ b/pkg/config/testdata/TestDbSettingsDiff/detects_differences.diff @@ -0,0 +1,10 @@ +diff remote[db.settings] local[db.settings] +--- remote[db.settings] ++++ local[db.settings] +@@ -1,3 +1,3 @@ +-effective_cache_size = "8GB" +-max_connections = 200 +-shared_buffers = "2GB" ++effective_cache_size = "4GB" ++max_connections = 100 ++shared_buffers = "1GB" diff --git a/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_local_side.diff b/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_local_side.diff new file mode 100644 index 000000000..392143fd1 --- /dev/null +++ b/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_local_side.diff @@ -0,0 +1,7 @@ +diff remote[db.settings] local[db.settings] +--- remote[db.settings] ++++ local[db.settings] +@@ -1,3 +0,0 @@ +-effective_cache_size = "4GB" +-max_connections = 100 +-shared_buffers = "1GB" diff --git a/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_remote_side.diff b/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_remote_side.diff new file mode 100644 index 000000000..938242538 --- /dev/null +++ b/pkg/config/testdata/TestDbSettingsDiff/handles_api_disabled_on_remote_side.diff @@ -0,0 +1,7 @@ +diff remote[db.settings] local[db.settings] +--- remote[db.settings] ++++ local[db.settings] +@@ -0,0 +1,3 @@ ++effective_cache_size = "4GB" ++max_connections = 100 ++shared_buffers = "1GB" diff --git a/pkg/config/testdata/TestEmailDiff/local_disabled_remote_enabled.diff b/pkg/config/testdata/TestEmailDiff/local_disabled_remote_enabled.diff new file mode 100644 index 000000000..d6c7f6dca --- /dev/null +++ b/pkg/config/testdata/TestEmailDiff/local_disabled_remote_enabled.diff @@ -0,0 +1,38 @@ +diff remote[auth] local[auth] +--- remote[auth] ++++ local[auth] +@@ -49,13 +49,13 @@ + inactivity_timeout = "0s" + + [email] +-enable_signup = true +-double_confirm_changes = true +-enable_confirmations = true +-secure_password_change = true +-max_frequency = "1s" +-otp_length = 6 +-otp_expiry = 3600 ++enable_signup = false ++double_confirm_changes = false ++enable_confirmations = false ++secure_password_change = false ++max_frequency = "1m0s" ++otp_length = 8 ++otp_expiry = 86400 + [email.template] + [email.template.confirmation] + content_path = "" +@@ -69,13 +69,6 @@ + content_path = "" + [email.template.recovery] + content_path = "" +-[email.smtp] +-host = "smtp.sendgrid.net" +-port = 587 +-user = "apikey" +-pass = "hash:ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e" +-admin_email = "admin@email.com" +-sender_name = "Admin" + + [sms] + enable_signup = false diff --git a/pkg/config/testdata/TestEmailDiff/local_enabled_remote_disabled.diff b/pkg/config/testdata/TestEmailDiff/local_enabled_remote_disabled.diff new file mode 100644 index 000000000..9bcf8ccba --- /dev/null +++ b/pkg/config/testdata/TestEmailDiff/local_enabled_remote_disabled.diff @@ -0,0 +1,55 @@ +diff remote[auth] local[auth] +--- remote[auth] ++++ local[auth] +@@ -49,28 +49,43 @@ + inactivity_timeout = "0s" + + [email] +-enable_signup = false +-double_confirm_changes = false +-enable_confirmations = false +-secure_password_change = false +-max_frequency = "1m0s" +-otp_length = 6 +-otp_expiry = 3600 ++enable_signup = true ++double_confirm_changes = true ++enable_confirmations = true ++secure_password_change = true ++max_frequency = "1s" ++otp_length = 8 ++otp_expiry = 86400 + [email.template] + [email.template.confirmation] ++subject = "confirmation-subject" + content_path = "" + [email.template.email_change] ++subject = "email-change-subject" + content = "email-change-content" + content_path = "" + [email.template.invite] ++subject = "invite-subject" ++content = "invite-content" + content_path = "" + [email.template.magic_link] + subject = "magic-link-subject" ++content = "magic-link-content" + content_path = "" + [email.template.reauthentication] ++subject = "" ++content = "" + content_path = "" + [email.template.recovery] +-content_path = "" ++content = "recovery-content" ++content_path = "" ++[email.smtp] ++host = "smtp.sendgrid.net" ++port = 587 ++user = "apikey" ++pass = "hash:ed64b7695a606bc6ab4fcb41fe815b5ddf1063ccbc87afe1fa89756635db520e" ++admin_email = "admin@email.com" ++sender_name = "Admin" + + [sms] + enable_signup = false diff --git a/pkg/config/testdata/TestExternalDiff/local_enabled_and_disabled.diff b/pkg/config/testdata/TestExternalDiff/local_enabled_and_disabled.diff new file mode 100644 index 000000000..fcfb3d1d0 --- /dev/null +++ b/pkg/config/testdata/TestExternalDiff/local_enabled_and_disabled.diff @@ -0,0 +1,21 @@ +diff remote[auth] local[auth] +--- remote[auth] ++++ local[auth] +@@ -88,7 +88,7 @@ + + [external] + [external.apple] +-enabled = false ++enabled = true + client_id = "test-client-1,test-client-2" + secret = "hash:ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252" + url = "" +@@ -144,7 +144,7 @@ + redirect_uri = "" + skip_nonce_check = false + [external.google] +-enabled = true ++enabled = false + client_id = "" + secret = "" + url = "" diff --git a/pkg/config/testdata/TestHookDiff/local_enabled_and_disabled.diff b/pkg/config/testdata/TestHookDiff/local_enabled_and_disabled.diff new file mode 100644 index 000000000..b45808d4c --- /dev/null +++ b/pkg/config/testdata/TestHookDiff/local_enabled_and_disabled.diff @@ -0,0 +1,21 @@ +diff remote[auth] local[auth] +--- remote[auth] ++++ local[auth] +@@ -9,7 +9,7 @@ + + [hook] + [hook.mfa_verification_attempt] +-enabled = true ++enabled = false + uri = "" + secrets = "" + [hook.password_verification_attempt] +@@ -17,7 +17,7 @@ + uri = "" + secrets = "" + [hook.custom_access_token] +-enabled = false ++enabled = true + uri = "" + secrets = "hash:b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad" + [hook.send_sms] diff --git a/pkg/config/testdata/TestMfaDiff/local_enabled_and_disabled.diff b/pkg/config/testdata/TestMfaDiff/local_enabled_and_disabled.diff new file mode 100644 index 000000000..f7935bfa4 --- /dev/null +++ b/pkg/config/testdata/TestMfaDiff/local_enabled_and_disabled.diff @@ -0,0 +1,26 @@ +diff remote[auth] local[auth] +--- remote[auth] ++++ local[auth] +@@ -30,16 +30,16 @@ + secrets = "" + + [mfa] +-max_enrolled_factors = 10 ++max_enrolled_factors = 0 + [mfa.totp] + enroll_enabled = false + verify_enabled = false + [mfa.phone] +-enroll_enabled = false +-verify_enabled = false +-otp_length = 6 +-template = "Your code is {{ .Code }}" +-max_frequency = "5s" ++enroll_enabled = true ++verify_enabled = true ++otp_length = 0 ++template = "" ++max_frequency = "0s" + [mfa.web_authn] + enroll_enabled = false + verify_enabled = false diff --git a/pkg/config/testdata/TestSmsDiff/enable_sign_up_without_provider.diff b/pkg/config/testdata/TestSmsDiff/enable_sign_up_without_provider.diff new file mode 100644 index 000000000..2e44496fd --- /dev/null +++ b/pkg/config/testdata/TestSmsDiff/enable_sign_up_without_provider.diff @@ -0,0 +1,12 @@ +diff remote[auth] local[auth] +--- remote[auth] ++++ local[auth] +@@ -58,7 +58,7 @@ + otp_expiry = 0 + + [sms] +-enable_signup = false ++enable_signup = true + enable_confirmations = false + template = "" + max_frequency = "0s" diff --git a/pkg/config/testdata/TestSmsDiff/local_disabled_remote_enabled.diff b/pkg/config/testdata/TestSmsDiff/local_disabled_remote_enabled.diff new file mode 100644 index 000000000..a173a8adf --- /dev/null +++ b/pkg/config/testdata/TestSmsDiff/local_disabled_remote_enabled.diff @@ -0,0 +1,31 @@ +diff remote[auth] local[auth] +--- remote[auth] ++++ local[auth] +@@ -58,12 +58,12 @@ + otp_expiry = 0 + + [sms] +-enable_signup = true +-enable_confirmations = true +-template = "Your code is {{ .Code }}" +-max_frequency = "1m0s" ++enable_signup = false ++enable_confirmations = false ++template = "" ++max_frequency = "0s" + [sms.twilio] +-enabled = true ++enabled = false + account_sid = "" + message_service_sid = "" + auth_token = "" +@@ -85,9 +85,6 @@ + from = "" + api_key = "" + api_secret = "" +-[sms.test_otp] +-123 = "456" +-456 = "123" + + [third_party] + [third_party.firebase] diff --git a/pkg/config/testdata/TestSmsDiff/local_enabled_remote_disabled.diff b/pkg/config/testdata/TestSmsDiff/local_enabled_remote_disabled.diff new file mode 100644 index 000000000..f9ee1d7f0 --- /dev/null +++ b/pkg/config/testdata/TestSmsDiff/local_enabled_remote_disabled.diff @@ -0,0 +1,43 @@ +diff remote[auth] local[auth] +--- remote[auth] ++++ local[auth] +@@ -58,12 +58,12 @@ + otp_expiry = 0 + + [sms] +-enable_signup = false +-enable_confirmations = false +-template = "" +-max_frequency = "0s" ++enable_signup = true ++enable_confirmations = true ++template = "Your code is {{ .Code }}" ++max_frequency = "1m0s" + [sms.twilio] +-enabled = true ++enabled = false + account_sid = "" + message_service_sid = "" + auth_token = "" +@@ -73,9 +73,9 @@ + message_service_sid = "" + auth_token = "" + [sms.messagebird] +-enabled = false +-originator = "" +-access_key = "hash:" ++enabled = true ++originator = "test-originator" ++access_key = "hash:ab60d03fc809fb02dae838582f3ddc13d1d6cb32ffba77c4b969dd3caa496f13" + [sms.textlocal] + enabled = false + sender = "" +@@ -85,6 +85,8 @@ + from = "" + api_key = "" + api_secret = "" ++[sms.test_otp] ++123 = "456" + + [third_party] + [third_party.firebase]