moved settings under models and added settings dao helpers

This commit is contained in:
Gani Georgiev 2022-11-26 14:42:45 +02:00
parent d8963c6fc3
commit 8c9b657132
14 changed files with 257 additions and 182 deletions

View File

@ -127,7 +127,7 @@ func TestSettingsSet(t *testing.T) {
`"twitchAuth":{`, `"twitchAuth":{`,
`"secret":"******"`, `"secret":"******"`,
`"clientSecret":"******"`, `"clientSecret":"******"`,
`"appName":"Acme"`, `"appName":"acme_test"`,
}, },
ExpectedEvents: map[string]int{ ExpectedEvents: map[string]int{
"OnModelBeforeUpdate": 1, "OnModelBeforeUpdate": 1,

View File

@ -6,6 +6,7 @@ package core
import ( import (
"github.com/pocketbase/dbx" "github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/mailer" "github.com/pocketbase/pocketbase/tools/mailer"
@ -47,7 +48,7 @@ type App interface {
IsDebug() bool IsDebug() bool
// Settings returns the loaded app settings. // Settings returns the loaded app settings.
Settings() *Settings Settings() *settings.Settings
// Cache returns the app internal cache store. // Cache returns the app internal cache store.
Cache() *store.Store[any] Cache() *store.Store[any]
@ -79,6 +80,14 @@ type App interface {
// App event hooks // App event hooks
// --------------------------------------------------------------- // ---------------------------------------------------------------
// OnBeforeBootstrap hook is triggered before initializing the base
// application resources (eg. before db open and initial settings load).
OnBeforeBootstrap() *hook.Hook[*BootstrapEvent]
// OnAfterBootstrap hook is triggered after initializing the base
// application resources (eg. after db open and initial settings load).
OnAfterBootstrap() *hook.Hook[*BootstrapEvent]
// OnBeforeServe hook is triggered before serving the internal router (echo), // OnBeforeServe hook is triggered before serving the internal router (echo),
// allowing you to adjust its options and attach new routes. // allowing you to adjust its options and attach new routes.
OnBeforeServe() *hook.Hook[*ServeEvent] OnBeforeServe() *hook.Hook[*ServeEvent]

View File

@ -1,10 +1,8 @@
package core package core
import ( import (
"bytes"
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"errors" "errors"
"log" "log"
"os" "os"
@ -15,10 +13,10 @@ import (
"github.com/pocketbase/dbx" "github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/mailer" "github.com/pocketbase/pocketbase/tools/mailer"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/store" "github.com/pocketbase/pocketbase/tools/store"
"github.com/pocketbase/pocketbase/tools/subscriptions" "github.com/pocketbase/pocketbase/tools/subscriptions"
) )
@ -34,15 +32,17 @@ type BaseApp struct {
// internals // internals
cache *store.Store[any] cache *store.Store[any]
settings *Settings settings *settings.Settings
db *dbx.DB db *dbx.DB
dao *daos.Dao dao *daos.Dao
logsDB *dbx.DB logsDB *dbx.DB
logsDao *daos.Dao logsDao *daos.Dao
subscriptionsBroker *subscriptions.Broker subscriptionsBroker *subscriptions.Broker
// serve event hooks // app event hooks
onBeforeServe *hook.Hook[*ServeEvent] onBeforeBootstrap *hook.Hook[*BootstrapEvent]
onAfterBootstrap *hook.Hook[*BootstrapEvent]
onBeforeServe *hook.Hook[*ServeEvent]
// dao event hooks // dao event hooks
onModelBeforeCreate *hook.Hook[*ModelEvent] onModelBeforeCreate *hook.Hook[*ModelEvent]
@ -125,11 +125,13 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
isDebug: isDebug, isDebug: isDebug,
encryptionEnv: encryptionEnv, encryptionEnv: encryptionEnv,
cache: store.New[any](nil), cache: store.New[any](nil),
settings: NewSettings(), settings: settings.New(),
subscriptionsBroker: subscriptions.NewBroker(), subscriptionsBroker: subscriptions.NewBroker(),
// serve event hooks // app event hooks
onBeforeServe: &hook.Hook[*ServeEvent]{}, onBeforeBootstrap: &hook.Hook[*BootstrapEvent]{},
onAfterBootstrap: &hook.Hook[*BootstrapEvent]{},
onBeforeServe: &hook.Hook[*ServeEvent]{},
// dao event hooks // dao event hooks
onModelBeforeCreate: &hook.Hook[*ModelEvent]{}, onModelBeforeCreate: &hook.Hook[*ModelEvent]{},
@ -210,6 +212,12 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
// Bootstrap initializes the application // Bootstrap initializes the application
// (aka. create data dir, open db connections, load settings, etc.) // (aka. create data dir, open db connections, load settings, etc.)
func (app *BaseApp) Bootstrap() error { func (app *BaseApp) Bootstrap() error {
event := &BootstrapEvent{app}
if err := app.OnBeforeBootstrap().Trigger(event); err != nil {
return err
}
// clear resources of previous core state (if any) // clear resources of previous core state (if any)
if err := app.ResetBootstrapState(); err != nil { if err := app.ResetBootstrapState(); err != nil {
return err return err
@ -228,10 +236,13 @@ func (app *BaseApp) Bootstrap() error {
return err return err
} }
// we don't check for an error because the db migrations may // we don't check for an error because the db migrations may have not been executed yet
// have not been executed yet.
app.RefreshSettings() app.RefreshSettings()
if err := app.OnAfterBootstrap().Trigger(event); err != nil && app.IsDebug() {
log.Println(err)
}
return nil return nil
} }
@ -295,7 +306,7 @@ func (app *BaseApp) IsDebug() bool {
} }
// Settings returns the loaded app settings. // Settings returns the loaded app settings.
func (app *BaseApp) Settings() *Settings { func (app *BaseApp) Settings() *settings.Settings {
return app.settings return app.settings
} }
@ -349,75 +360,41 @@ func (app *BaseApp) NewFilesystem() (*filesystem.System, error) {
// RefreshSettings reinitializes and reloads the stored application settings. // RefreshSettings reinitializes and reloads the stored application settings.
func (app *BaseApp) RefreshSettings() error { func (app *BaseApp) RefreshSettings() error {
if app.settings == nil { if app.settings == nil {
app.settings = NewSettings() app.settings = settings.New()
} }
encryptionKey := os.Getenv(app.EncryptionEnv()) encryptionKey := os.Getenv(app.EncryptionEnv())
param, err := app.Dao().FindParamByKey(models.ParamAppSettings) storedSettings, err := app.Dao().FindSettings(encryptionKey)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return err return err
} }
// no settings were previously stored // no settings were previously stored
if param == nil { if storedSettings == nil {
return app.Dao().SaveParam(models.ParamAppSettings, app.settings, encryptionKey) return app.Dao().SaveSettings(app.settings, encryptionKey)
} }
// load the settings from the stored param into the app ones // load the settings from the stored param into the app ones
// --- if err := app.settings.Merge(storedSettings); err != nil {
newSettings := NewSettings()
// try first without decryption
plainDecodeErr := json.Unmarshal(param.Value, newSettings)
// failed, try to decrypt
if plainDecodeErr != nil {
// load without decrypt has failed and there is no encryption key to use for decrypt
if encryptionKey == "" {
return errors.New("Failed to load the stored app settings (missing or invalid encryption key).")
}
// decrypt
decrypted, decryptErr := security.Decrypt(string(param.Value), encryptionKey)
if decryptErr != nil {
return decryptErr
}
// decode again
decryptedDecodeErr := json.Unmarshal(decrypted, newSettings)
if decryptedDecodeErr != nil {
return decryptedDecodeErr
}
}
if err := app.settings.Merge(newSettings); err != nil {
return err return err
} }
afterMergeRaw, err := json.Marshal(app.settings)
if err != nil {
return err
}
if
// save because previously the settings weren't stored encrypted
(plainDecodeErr == nil && encryptionKey != "") ||
// or save because there are new fields after the merge
!bytes.Equal(param.Value, afterMergeRaw) {
saveErr := app.Dao().SaveParam(models.ParamAppSettings, app.settings, encryptionKey)
if saveErr != nil {
return saveErr
}
}
return nil return nil
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Serve event hooks // App event hooks
// ------------------------------------------------------------------- // -------------------------------------------------------------------
func (app *BaseApp) OnBeforeBootstrap() *hook.Hook[*BootstrapEvent] {
return app.onBeforeBootstrap
}
func (app *BaseApp) OnAfterBootstrap() *hook.Hook[*BootstrapEvent] {
return app.onAfterBootstrap
}
func (app *BaseApp) OnBeforeServe() *hook.Hook[*ServeEvent] { func (app *BaseApp) OnBeforeServe() *hook.Hook[*ServeEvent] {
return app.onBeforeServe return app.onBeforeServe
} }

View File

@ -1,7 +1,6 @@
package core_test package core_test
import ( import (
"bytes"
"testing" "testing"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
@ -32,7 +31,7 @@ func TestBaseAppRefreshSettings(t *testing.T) {
t.Fatalf("Expected new settings to be persisted, got %v", err) t.Fatalf("Expected new settings to be persisted, got %v", err)
} }
// change the db entry and refresh the app settings // change the db entry and refresh the app settings (ensure that there was no db update)
param.Value = types.JsonRaw([]byte(`{"example": 123}`)) param.Value = types.JsonRaw([]byte(`{"example": 123}`))
if err := app.Dao().SaveParam(param.Key, param.Value); err != nil { if err := app.Dao().SaveParam(param.Key, param.Value); err != nil {
t.Fatalf("Failed to update the test settings: %v", err) t.Fatalf("Failed to update the test settings: %v", err)
@ -41,21 +40,9 @@ func TestBaseAppRefreshSettings(t *testing.T) {
if err := app.RefreshSettings(); err != nil { if err := app.RefreshSettings(); err != nil {
t.Fatalf("Failed to refresh the app settings: %v", err) t.Fatalf("Failed to refresh the app settings: %v", err)
} }
testEventCalls(t, app, map[string]int{ testEventCalls(t, app, nil)
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
})
// make sure that the newly merged settings were actually saved // try to refresh again without doing any changes
newParam, err := app.Dao().FindParamByKey(models.ParamAppSettings)
if err != nil {
t.Fatalf("Failed to fetch new settings param: %v", err)
}
if bytes.Equal(param.Value, newParam.Value) {
t.Fatalf("Expected the new refreshed settings to be different, got: \n%v", string(newParam.Value))
}
// try to refresh again and ensure that there was no db update
app.ResetEventCalls() app.ResetEventCalls()
if err := app.RefreshSettings(); err != nil { if err := app.RefreshSettings(); err != nil {
t.Fatalf("Failed to refresh the app settings without change: %v", err) t.Fatalf("Failed to refresh the app settings without change: %v", err)

View File

@ -4,6 +4,7 @@ import (
"github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tools/mailer" "github.com/pocketbase/pocketbase/tools/mailer"
"github.com/pocketbase/pocketbase/tools/search" "github.com/pocketbase/pocketbase/tools/search"
"github.com/pocketbase/pocketbase/tools/subscriptions" "github.com/pocketbase/pocketbase/tools/subscriptions"
@ -15,6 +16,10 @@ import (
// Serve events data // Serve events data
// ------------------------------------------------------------------- // -------------------------------------------------------------------
type BootstrapEvent struct {
App App
}
type ServeEvent struct { type ServeEvent struct {
App App App App
Router *echo.Echo Router *echo.Echo
@ -68,13 +73,13 @@ type RealtimeSubscribeEvent struct {
type SettingsListEvent struct { type SettingsListEvent struct {
HttpContext echo.Context HttpContext echo.Context
RedactedSettings *Settings RedactedSettings *settings.Settings
} }
type SettingsUpdateEvent struct { type SettingsUpdateEvent struct {
HttpContext echo.Context HttpContext echo.Context
OldSettings *Settings OldSettings *settings.Settings
NewSettings *Settings NewSettings *settings.Settings
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------

63
daos/settings.go Normal file
View File

@ -0,0 +1,63 @@
package daos
import (
"encoding/json"
"errors"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tools/security"
)
// FindSettings returns and decode the serialized app settings param value.
//
// The method will first try to decode the param value without decryption.
// If it fails and optEncryptionKey is set, it will try again by first
// decrypting the value and then decode it again.
//
// Returns an error if it fails to decode the stored serialized param value.
func (dao *Dao) FindSettings(optEncryptionKey ...string) (*settings.Settings, error) {
param, err := dao.FindParamByKey(models.ParamAppSettings)
if err != nil {
return nil, err
}
result := settings.New()
// try first without decryption
plainDecodeErr := json.Unmarshal(param.Value, result)
// failed, try to decrypt
if plainDecodeErr != nil {
var encryptionKey string
if len(optEncryptionKey) > 0 && optEncryptionKey[0] != "" {
encryptionKey = optEncryptionKey[0]
}
// load without decrypt has failed and there is no encryption key to use for decrypt
if encryptionKey == "" {
return nil, errors.New("failed to load the stored app settings - missing or invalid encryption key")
}
// decrypt
decrypted, decryptErr := security.Decrypt(string(param.Value), encryptionKey)
if decryptErr != nil {
return nil, decryptErr
}
// decode again
decryptedDecodeErr := json.Unmarshal(decrypted, result)
if decryptedDecodeErr != nil {
return nil, decryptedDecodeErr
}
}
return result, nil
}
// SaveSettings persists the specified settings configuration.
//
// If optEncryptionKey is set, then the stored serialized value will be encrypted with it.
func (dao *Dao) SaveSettings(newSettings *settings.Settings, optEncryptionKey ...string) error {
return dao.SaveParam(models.ParamAppSettings, newSettings, optEncryptionKey...)
}

50
daos/settings_test.go Normal file
View File

@ -0,0 +1,50 @@
package daos_test
import (
"testing"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/security"
)
func TestSaveAndFindSettings(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
encryptionKey := security.PseudorandomString(32)
// change unencrypted app settings
app.Settings().Meta.AppName = "save_unencrypted"
if err := app.Dao().SaveSettings(app.Settings()); err != nil {
t.Fatal(err)
}
// check if the change was persisted
s1, err := app.Dao().FindSettings()
if err != nil {
t.Fatalf("Failed to fetch settings: %v", err)
}
if s1.Meta.AppName != "save_unencrypted" {
t.Fatalf("Expected settings to be changed with app name %q, got \n%v", "save_unencrypted", s1)
}
// make another change but this time provide an encryption key
app.Settings().Meta.AppName = "save_encrypted"
if err := app.Dao().SaveSettings(app.Settings(), encryptionKey); err != nil {
t.Fatal(err)
}
// try to fetch the settings without encryption key (should fail)
if s2, err := app.Dao().FindSettings(); err == nil {
t.Fatalf("Expected FindSettings to fail without an encryption key, got \n%v", s2)
}
// try again but this time with an encryption key
s3, err := app.Dao().FindSettings(encryptionKey)
if err != nil {
t.Fatalf("Failed to fetch settings with an encryption key %s: %v", encryptionKey, err)
}
if s3.Meta.AppName != "save_encrypted" {
t.Fatalf("Expected settings to be changed with app name %q, got \n%v", "save_encrypted", s3)
}
}

View File

@ -6,12 +6,12 @@ import (
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models/settings"
) )
// SettingsUpsert is a [core.Settings] upsert (create/update) form. // SettingsUpsert is a [settings.Settings] upsert (create/update) form.
type SettingsUpsert struct { type SettingsUpsert struct {
*core.Settings *settings.Settings
app core.App app core.App
dao *daos.Dao dao *daos.Dao
@ -55,16 +55,10 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error {
return err return err
} }
encryptionKey := os.Getenv(form.app.EncryptionEnv())
return runInterceptors(func() error { return runInterceptors(func() error {
saveErr := form.dao.SaveParam( encryptionKey := os.Getenv(form.app.EncryptionEnv())
models.ParamAppSettings, if err := form.dao.SaveSettings(form.Settings, encryptionKey); err != nil {
form.Settings, return err
encryptionKey,
)
if saveErr != nil {
return saveErr
} }
// explicitly trigger old logs deletion // explicitly trigger old logs deletion
@ -73,7 +67,7 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error {
) )
if form.Settings.Logs.MaxDays == 0 { if form.Settings.Logs.MaxDays == 0 {
// reclaim deleted logs disk space // no logs are allowed -> reclaim preserved disk space after the previous delete operation
form.app.LogsDao().Vacuum() form.app.LogsDao().Vacuum()
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/mails/templates" "github.com/pocketbase/pocketbase/mails/templates"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tokens" "github.com/pocketbase/pocketbase/tokens"
"github.com/pocketbase/pocketbase/tools/mailer" "github.com/pocketbase/pocketbase/tools/mailer"
) )
@ -20,17 +21,15 @@ func SendRecordPasswordReset(app core.App, authRecord *models.Record) error {
mailClient := app.NewMailClient() mailClient := app.NewMailClient()
settings := app.Settings() subject, body, err := resolveEmailTemplate(app, token, app.Settings().Meta.ResetPasswordTemplate)
subject, body, err := resolveEmailTemplate(app, token, settings.Meta.ResetPasswordTemplate)
if err != nil { if err != nil {
return err return err
} }
message := &mailer.Message{ message := &mailer.Message{
From: mail.Address{ From: mail.Address{
Name: settings.Meta.SenderName, Name: app.Settings().Meta.SenderName,
Address: settings.Meta.SenderAddress, Address: app.Settings().Meta.SenderAddress,
}, },
To: mail.Address{Address: authRecord.Email()}, To: mail.Address{Address: authRecord.Email()},
Subject: subject, Subject: subject,
@ -64,17 +63,15 @@ func SendRecordVerification(app core.App, authRecord *models.Record) error {
mailClient := app.NewMailClient() mailClient := app.NewMailClient()
settings := app.Settings() subject, body, err := resolveEmailTemplate(app, token, app.Settings().Meta.VerificationTemplate)
subject, body, err := resolveEmailTemplate(app, token, settings.Meta.VerificationTemplate)
if err != nil { if err != nil {
return err return err
} }
message := &mailer.Message{ message := &mailer.Message{
From: mail.Address{ From: mail.Address{
Name: settings.Meta.SenderName, Name: app.Settings().Meta.SenderName,
Address: settings.Meta.SenderAddress, Address: app.Settings().Meta.SenderAddress,
}, },
To: mail.Address{Address: authRecord.Email()}, To: mail.Address{Address: authRecord.Email()},
Subject: subject, Subject: subject,
@ -108,17 +105,15 @@ func SendRecordChangeEmail(app core.App, record *models.Record, newEmail string)
mailClient := app.NewMailClient() mailClient := app.NewMailClient()
settings := app.Settings() subject, body, err := resolveEmailTemplate(app, token, app.Settings().Meta.ConfirmEmailChangeTemplate)
subject, body, err := resolveEmailTemplate(app, token, settings.Meta.ConfirmEmailChangeTemplate)
if err != nil { if err != nil {
return err return err
} }
message := &mailer.Message{ message := &mailer.Message{
From: mail.Address{ From: mail.Address{
Name: settings.Meta.SenderName, Name: app.Settings().Meta.SenderName,
Address: settings.Meta.SenderAddress, Address: app.Settings().Meta.SenderAddress,
}, },
To: mail.Address{Address: newEmail}, To: mail.Address{Address: newEmail},
Subject: subject, Subject: subject,
@ -149,13 +144,11 @@ func SendRecordChangeEmail(app core.App, record *models.Record, newEmail string)
func resolveEmailTemplate( func resolveEmailTemplate(
app core.App, app core.App,
token string, token string,
emailTemplate core.EmailTemplate, emailTemplate settings.EmailTemplate,
) (subject string, body string, err error) { ) (subject string, body string, err error) {
settings := app.Settings()
subject, rawBody, _ := emailTemplate.Resolve( subject, rawBody, _ := emailTemplate.Resolve(
settings.Meta.AppName, app.Settings().Meta.AppName,
settings.Meta.AppUrl, app.Settings().Meta.AppUrl,
token, token,
) )

View File

@ -12,13 +12,10 @@ package templates
const AdminPasswordResetBody = ` const AdminPasswordResetBody = `
{{define "content"}} {{define "content"}}
<p>Hello,</p> <p>Hello,</p>
<p>Follow this link to reset your admin password for {{.AppName}}.</p> <p>Follow this link to reset your admin password for {{.AppName}}.</p>
<p> <p>
<a class="btn" href="{{.ActionUrl}}" target="_blank" rel="noopener">Reset password</a> <a class="btn" href="{{.ActionUrl}}" target="_blank" rel="noopener">Reset password</a>
</p> </p>
<p><i>If you did not request to reset your password, please ignore this email and the link will expire on its own.</i></p> <p><i>If you did not request to reset your password, please ignore this email and the link will expire on its own.</i></p>
{{end}} {{end}}
` `

View File

@ -1,4 +1,4 @@
package core package settings
import ( import (
"encoding/json" "encoding/json"
@ -45,8 +45,8 @@ type Settings struct {
TwitchAuth AuthProviderConfig `form:"twitchAuth" json:"twitchAuth"` TwitchAuth AuthProviderConfig `form:"twitchAuth" json:"twitchAuth"`
} }
// NewSettings creates and returns a new default Settings instance. // New creates and returns a new default Settings instance.
func NewSettings() *Settings { func New() *Settings {
return &Settings{ return &Settings{
Meta: MetaConfig{ Meta: MetaConfig{
AppName: "Acme", AppName: "Acme",
@ -170,11 +170,11 @@ func (s *Settings) Merge(other *Settings) error {
// Clone creates a new deep copy of the current settings. // Clone creates a new deep copy of the current settings.
func (s *Settings) Clone() (*Settings, error) { func (s *Settings) Clone() (*Settings, error) {
settings := &Settings{} clone := &Settings{}
if err := settings.Merge(s); err != nil { if err := clone.Merge(s); err != nil {
return nil, err return nil, err
} }
return settings, nil return clone, nil
} }
// RedactClone creates a new deep copy of the current settings, // RedactClone creates a new deep copy of the current settings,

View File

@ -1,4 +1,4 @@
package core package settings
// Common settings placeholder tokens // Common settings placeholder tokens
const ( const (

View File

@ -1,4 +1,4 @@
package core_test package settings_test
import ( import (
"encoding/json" "encoding/json"
@ -7,12 +7,12 @@ import (
"testing" "testing"
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tools/auth" "github.com/pocketbase/pocketbase/tools/auth"
) )
func TestSettingsValidate(t *testing.T) { func TestSettingsValidate(t *testing.T) {
s := core.NewSettings() s := settings.New()
// set invalid settings data // set invalid settings data
s.Meta.AppName = "" s.Meta.AppName = ""
@ -87,10 +87,10 @@ func TestSettingsValidate(t *testing.T) {
} }
func TestSettingsMerge(t *testing.T) { func TestSettingsMerge(t *testing.T) {
s1 := core.NewSettings() s1 := settings.New()
s1.Meta.AppUrl = "old_app_url" s1.Meta.AppUrl = "old_app_url"
s2 := core.NewSettings() s2 := settings.New()
s2.Meta.AppName = "test" s2.Meta.AppName = "test"
s2.Logs.MaxDays = 123 s2.Logs.MaxDays = 123
s2.Smtp.Host = "test" s2.Smtp.Host = "test"
@ -144,7 +144,7 @@ func TestSettingsMerge(t *testing.T) {
} }
func TestSettingsClone(t *testing.T) { func TestSettingsClone(t *testing.T) {
s1 := core.NewSettings() s1 := settings.New()
s2, err := s1.Clone() s2, err := s1.Clone()
if err != nil { if err != nil {
@ -173,7 +173,7 @@ func TestSettingsClone(t *testing.T) {
} }
func TestSettingsRedactClone(t *testing.T) { func TestSettingsRedactClone(t *testing.T) {
s1 := core.NewSettings() s1 := settings.New()
s1.Meta.AppName = "test123" // control field s1.Meta.AppName = "test123" // control field
s1.Smtp.Password = "test123" s1.Smtp.Password = "test123"
s1.Smtp.Tls = true s1.Smtp.Tls = true
@ -213,7 +213,7 @@ func TestSettingsRedactClone(t *testing.T) {
} }
func TestNamedAuthProviderConfigs(t *testing.T) { func TestNamedAuthProviderConfigs(t *testing.T) {
s := core.NewSettings() s := settings.New()
s.GoogleAuth.ClientId = "google_test" s.GoogleAuth.ClientId = "google_test"
s.FacebookAuth.ClientId = "facebook_test" s.FacebookAuth.ClientId = "facebook_test"
@ -256,17 +256,17 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
func TestTokenConfigValidate(t *testing.T) { func TestTokenConfigValidate(t *testing.T) {
scenarios := []struct { scenarios := []struct {
config core.TokenConfig config settings.TokenConfig
expectError bool expectError bool
}{ }{
// zero values // zero values
{ {
core.TokenConfig{}, settings.TokenConfig{},
true, true,
}, },
// invalid data // invalid data
{ {
core.TokenConfig{ settings.TokenConfig{
Secret: strings.Repeat("a", 5), Secret: strings.Repeat("a", 5),
Duration: 4, Duration: 4,
}, },
@ -274,7 +274,7 @@ func TestTokenConfigValidate(t *testing.T) {
}, },
// valid secret but invalid duration // valid secret but invalid duration
{ {
core.TokenConfig{ settings.TokenConfig{
Secret: strings.Repeat("a", 30), Secret: strings.Repeat("a", 30),
Duration: 63072000 + 1, Duration: 63072000 + 1,
}, },
@ -282,7 +282,7 @@ func TestTokenConfigValidate(t *testing.T) {
}, },
// valid data // valid data
{ {
core.TokenConfig{ settings.TokenConfig{
Secret: strings.Repeat("a", 30), Secret: strings.Repeat("a", 30),
Duration: 100, Duration: 100,
}, },
@ -305,22 +305,22 @@ func TestTokenConfigValidate(t *testing.T) {
func TestSmtpConfigValidate(t *testing.T) { func TestSmtpConfigValidate(t *testing.T) {
scenarios := []struct { scenarios := []struct {
config core.SmtpConfig config settings.SmtpConfig
expectError bool expectError bool
}{ }{
// zero values (disabled) // zero values (disabled)
{ {
core.SmtpConfig{}, settings.SmtpConfig{},
false, false,
}, },
// zero values (enabled) // zero values (enabled)
{ {
core.SmtpConfig{Enabled: true}, settings.SmtpConfig{Enabled: true},
true, true,
}, },
// invalid data // invalid data
{ {
core.SmtpConfig{ settings.SmtpConfig{
Enabled: true, Enabled: true,
Host: "test:test:test", Host: "test:test:test",
Port: -10, Port: -10,
@ -329,7 +329,7 @@ func TestSmtpConfigValidate(t *testing.T) {
}, },
// valid data // valid data
{ {
core.SmtpConfig{ settings.SmtpConfig{
Enabled: true, Enabled: true,
Host: "example.com", Host: "example.com",
Port: 100, Port: 100,
@ -354,22 +354,22 @@ func TestSmtpConfigValidate(t *testing.T) {
func TestS3ConfigValidate(t *testing.T) { func TestS3ConfigValidate(t *testing.T) {
scenarios := []struct { scenarios := []struct {
config core.S3Config config settings.S3Config
expectError bool expectError bool
}{ }{
// zero values (disabled) // zero values (disabled)
{ {
core.S3Config{}, settings.S3Config{},
false, false,
}, },
// zero values (enabled) // zero values (enabled)
{ {
core.S3Config{Enabled: true}, settings.S3Config{Enabled: true},
true, true,
}, },
// invalid data // invalid data
{ {
core.S3Config{ settings.S3Config{
Enabled: true, Enabled: true,
Endpoint: "test:test:test", Endpoint: "test:test:test",
}, },
@ -377,7 +377,7 @@ func TestS3ConfigValidate(t *testing.T) {
}, },
// valid data (url endpoint) // valid data (url endpoint)
{ {
core.S3Config{ settings.S3Config{
Enabled: true, Enabled: true,
Endpoint: "https://localhost:8090", Endpoint: "https://localhost:8090",
Bucket: "test", Bucket: "test",
@ -389,7 +389,7 @@ func TestS3ConfigValidate(t *testing.T) {
}, },
// valid data (hostname endpoint) // valid data (hostname endpoint)
{ {
core.S3Config{ settings.S3Config{
Enabled: true, Enabled: true,
Endpoint: "example.com", Endpoint: "example.com",
Bucket: "test", Bucket: "test",
@ -415,36 +415,36 @@ func TestS3ConfigValidate(t *testing.T) {
} }
func TestMetaConfigValidate(t *testing.T) { func TestMetaConfigValidate(t *testing.T) {
invalidTemplate := core.EmailTemplate{ invalidTemplate := settings.EmailTemplate{
Subject: "test", Subject: "test",
ActionUrl: "test", ActionUrl: "test",
Body: "test", Body: "test",
} }
noPlaceholdersTemplate := core.EmailTemplate{ noPlaceholdersTemplate := settings.EmailTemplate{
Subject: "test", Subject: "test",
ActionUrl: "http://example.com", ActionUrl: "http://example.com",
Body: "test", Body: "test",
} }
withPlaceholdersTemplate := core.EmailTemplate{ withPlaceholdersTemplate := settings.EmailTemplate{
Subject: "test", Subject: "test",
ActionUrl: "http://example.com" + core.EmailPlaceholderToken, ActionUrl: "http://example.com" + settings.EmailPlaceholderToken,
Body: "test" + core.EmailPlaceholderActionUrl, Body: "test" + settings.EmailPlaceholderActionUrl,
} }
scenarios := []struct { scenarios := []struct {
config core.MetaConfig config settings.MetaConfig
expectError bool expectError bool
}{ }{
// zero values // zero values
{ {
core.MetaConfig{}, settings.MetaConfig{},
true, true,
}, },
// invalid data // invalid data
{ {
core.MetaConfig{ settings.MetaConfig{
AppName: strings.Repeat("a", 300), AppName: strings.Repeat("a", 300),
AppUrl: "test", AppUrl: "test",
SenderName: strings.Repeat("a", 300), SenderName: strings.Repeat("a", 300),
@ -457,7 +457,7 @@ func TestMetaConfigValidate(t *testing.T) {
}, },
// invalid data (missing required placeholders) // invalid data (missing required placeholders)
{ {
core.MetaConfig{ settings.MetaConfig{
AppName: "test", AppName: "test",
AppUrl: "https://example.com", AppUrl: "https://example.com",
SenderName: "test", SenderName: "test",
@ -470,7 +470,7 @@ func TestMetaConfigValidate(t *testing.T) {
}, },
// valid data // valid data
{ {
core.MetaConfig{ settings.MetaConfig{
AppName: "test", AppName: "test",
AppUrl: "https://example.com", AppUrl: "https://example.com",
SenderName: "test", SenderName: "test",
@ -498,17 +498,17 @@ func TestMetaConfigValidate(t *testing.T) {
func TestEmailTemplateValidate(t *testing.T) { func TestEmailTemplateValidate(t *testing.T) {
scenarios := []struct { scenarios := []struct {
emailTemplate core.EmailTemplate emailTemplate settings.EmailTemplate
expectedErrors []string expectedErrors []string
}{ }{
// require values // require values
{ {
core.EmailTemplate{}, settings.EmailTemplate{},
[]string{"subject", "actionUrl", "body"}, []string{"subject", "actionUrl", "body"},
}, },
// missing placeholders // missing placeholders
{ {
core.EmailTemplate{ settings.EmailTemplate{
Subject: "test", Subject: "test",
ActionUrl: "test", ActionUrl: "test",
Body: "test", Body: "test",
@ -517,10 +517,10 @@ func TestEmailTemplateValidate(t *testing.T) {
}, },
// valid data // valid data
{ {
core.EmailTemplate{ settings.EmailTemplate{
Subject: "test", Subject: "test",
ActionUrl: "test" + core.EmailPlaceholderToken, ActionUrl: "test" + settings.EmailPlaceholderToken,
Body: "test" + core.EmailPlaceholderActionUrl, Body: "test" + settings.EmailPlaceholderActionUrl,
}, },
[]string{}, []string{},
}, },
@ -549,17 +549,17 @@ func TestEmailTemplateValidate(t *testing.T) {
} }
func TestEmailTemplateResolve(t *testing.T) { func TestEmailTemplateResolve(t *testing.T) {
allPlaceholders := core.EmailPlaceholderActionUrl + core.EmailPlaceholderToken + core.EmailPlaceholderAppName + core.EmailPlaceholderAppUrl allPlaceholders := settings.EmailPlaceholderActionUrl + settings.EmailPlaceholderToken + settings.EmailPlaceholderAppName + settings.EmailPlaceholderAppUrl
scenarios := []struct { scenarios := []struct {
emailTemplate core.EmailTemplate emailTemplate settings.EmailTemplate
expectedSubject string expectedSubject string
expectedBody string expectedBody string
expectedActionUrl string expectedActionUrl string
}{ }{
// no placeholders // no placeholders
{ {
emailTemplate: core.EmailTemplate{ emailTemplate: settings.EmailTemplate{
Subject: "subject:", Subject: "subject:",
Body: "body:", Body: "body:",
ActionUrl: "/actionUrl////", ActionUrl: "/actionUrl////",
@ -570,7 +570,7 @@ func TestEmailTemplateResolve(t *testing.T) {
}, },
// with placeholders // with placeholders
{ {
emailTemplate: core.EmailTemplate{ emailTemplate: settings.EmailTemplate{
ActionUrl: "/actionUrl////" + allPlaceholders, ActionUrl: "/actionUrl////" + allPlaceholders,
Subject: "subject:" + allPlaceholders, Subject: "subject:" + allPlaceholders,
Body: "body:" + allPlaceholders, Body: "body:" + allPlaceholders,
@ -583,8 +583,8 @@ func TestEmailTemplateResolve(t *testing.T) {
), ),
expectedSubject: fmt.Sprintf( expectedSubject: fmt.Sprintf(
"subject:%s%s%s%s", "subject:%s%s%s%s",
core.EmailPlaceholderActionUrl, settings.EmailPlaceholderActionUrl,
core.EmailPlaceholderToken, settings.EmailPlaceholderToken,
"name_test", "name_test",
"url_test", "url_test",
), ),
@ -622,22 +622,22 @@ func TestEmailTemplateResolve(t *testing.T) {
func TestLogsConfigValidate(t *testing.T) { func TestLogsConfigValidate(t *testing.T) {
scenarios := []struct { scenarios := []struct {
config core.LogsConfig config settings.LogsConfig
expectError bool expectError bool
}{ }{
// zero values // zero values
{ {
core.LogsConfig{}, settings.LogsConfig{},
false, false,
}, },
// invalid data // invalid data
{ {
core.LogsConfig{MaxDays: -10}, settings.LogsConfig{MaxDays: -10},
true, true,
}, },
// valid data // valid data
{ {
core.LogsConfig{MaxDays: 1}, settings.LogsConfig{MaxDays: 1},
false, false,
}, },
} }
@ -657,22 +657,22 @@ func TestLogsConfigValidate(t *testing.T) {
func TestAuthProviderConfigValidate(t *testing.T) { func TestAuthProviderConfigValidate(t *testing.T) {
scenarios := []struct { scenarios := []struct {
config core.AuthProviderConfig config settings.AuthProviderConfig
expectError bool expectError bool
}{ }{
// zero values (disabled) // zero values (disabled)
{ {
core.AuthProviderConfig{}, settings.AuthProviderConfig{},
false, false,
}, },
// zero values (enabled) // zero values (enabled)
{ {
core.AuthProviderConfig{Enabled: true}, settings.AuthProviderConfig{Enabled: true},
true, true,
}, },
// invalid data // invalid data
{ {
core.AuthProviderConfig{ settings.AuthProviderConfig{
Enabled: true, Enabled: true,
ClientId: "", ClientId: "",
ClientSecret: "", ClientSecret: "",
@ -684,7 +684,7 @@ func TestAuthProviderConfigValidate(t *testing.T) {
}, },
// valid data (only the required) // valid data (only the required)
{ {
core.AuthProviderConfig{ settings.AuthProviderConfig{
Enabled: true, Enabled: true,
ClientId: "test", ClientId: "test",
ClientSecret: "test", ClientSecret: "test",
@ -693,7 +693,7 @@ func TestAuthProviderConfigValidate(t *testing.T) {
}, },
// valid data (fill all fields) // valid data (fill all fields)
{ {
core.AuthProviderConfig{ settings.AuthProviderConfig{
Enabled: true, Enabled: true,
ClientId: "test", ClientId: "test",
ClientSecret: "test", ClientSecret: "test",
@ -722,12 +722,12 @@ func TestAuthProviderConfigSetupProvider(t *testing.T) {
provider := auth.NewGithubProvider() provider := auth.NewGithubProvider()
// disabled config // disabled config
c1 := core.AuthProviderConfig{Enabled: false} c1 := settings.AuthProviderConfig{Enabled: false}
if err := c1.SetupProvider(provider); err == nil { if err := c1.SetupProvider(provider); err == nil {
t.Errorf("Expected error, got nil") t.Errorf("Expected error, got nil")
} }
c2 := core.AuthProviderConfig{ c2 := settings.AuthProviderConfig{
Enabled: true, Enabled: true,
ClientId: "test_ClientId", ClientId: "test_ClientId",
ClientSecret: "test_ClientSecret", ClientSecret: "test_ClientSecret",

Binary file not shown.