Skip to content

Commit

Permalink
moved settings under models and added settings dao helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Nov 26, 2022
1 parent d8963c6 commit 8c9b657
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 182 deletions.
2 changes: 1 addition & 1 deletion apis/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func TestSettingsSet(t *testing.T) {
`"twitchAuth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
`"appName":"Acme"`,
`"appName":"acme_test"`,
},
ExpectedEvents: map[string]int{
"OnModelBeforeUpdate": 1,
Expand Down
11 changes: 10 additions & 1 deletion core/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package core
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/mailer"
Expand Down Expand Up @@ -47,7 +48,7 @@ type App interface {
IsDebug() bool

// Settings returns the loaded app settings.
Settings() *Settings
Settings() *settings.Settings

// Cache returns the app internal cache store.
Cache() *store.Store[any]
Expand Down Expand Up @@ -79,6 +80,14 @@ type App interface {
// 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),
// allowing you to adjust its options and attach new routes.
OnBeforeServe() *hook.Hook[*ServeEvent]
Expand Down
97 changes: 37 additions & 60 deletions core/base.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package core

import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"log"
"os"
Expand All @@ -15,10 +13,10 @@ import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/mailer"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/store"
"github.com/pocketbase/pocketbase/tools/subscriptions"
)
Expand All @@ -34,15 +32,17 @@ type BaseApp struct {

// internals
cache *store.Store[any]
settings *Settings
settings *settings.Settings
db *dbx.DB
dao *daos.Dao
logsDB *dbx.DB
logsDao *daos.Dao
subscriptionsBroker *subscriptions.Broker

// serve event hooks
onBeforeServe *hook.Hook[*ServeEvent]
// app event hooks
onBeforeBootstrap *hook.Hook[*BootstrapEvent]
onAfterBootstrap *hook.Hook[*BootstrapEvent]
onBeforeServe *hook.Hook[*ServeEvent]

// dao event hooks
onModelBeforeCreate *hook.Hook[*ModelEvent]
Expand Down Expand Up @@ -125,11 +125,13 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
isDebug: isDebug,
encryptionEnv: encryptionEnv,
cache: store.New[any](nil),
settings: NewSettings(),
settings: settings.New(),
subscriptionsBroker: subscriptions.NewBroker(),

// serve event hooks
onBeforeServe: &hook.Hook[*ServeEvent]{},
// app event hooks
onBeforeBootstrap: &hook.Hook[*BootstrapEvent]{},
onAfterBootstrap: &hook.Hook[*BootstrapEvent]{},
onBeforeServe: &hook.Hook[*ServeEvent]{},

// dao event hooks
onModelBeforeCreate: &hook.Hook[*ModelEvent]{},
Expand Down Expand Up @@ -210,6 +212,12 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
// Bootstrap initializes the application
// (aka. create data dir, open db connections, load settings, etc.)
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)
if err := app.ResetBootstrapState(); err != nil {
return err
Expand All @@ -228,10 +236,13 @@ func (app *BaseApp) Bootstrap() error {
return err
}

// we don't check for an error because the db migrations may
// have not been executed yet.
// we don't check for an error because the db migrations may have not been executed yet
app.RefreshSettings()

if err := app.OnAfterBootstrap().Trigger(event); err != nil && app.IsDebug() {
log.Println(err)
}

return nil
}

Expand Down Expand Up @@ -295,7 +306,7 @@ func (app *BaseApp) IsDebug() bool {
}

// Settings returns the loaded app settings.
func (app *BaseApp) Settings() *Settings {
func (app *BaseApp) Settings() *settings.Settings {
return app.settings
}

Expand Down Expand Up @@ -349,75 +360,41 @@ func (app *BaseApp) NewFilesystem() (*filesystem.System, error) {
// RefreshSettings reinitializes and reloads the stored application settings.
func (app *BaseApp) RefreshSettings() error {
if app.settings == nil {
app.settings = NewSettings()
app.settings = settings.New()
}

encryptionKey := os.Getenv(app.EncryptionEnv())

param, err := app.Dao().FindParamByKey(models.ParamAppSettings)
storedSettings, err := app.Dao().FindSettings(encryptionKey)
if err != nil && err != sql.ErrNoRows {
return err
}

// no settings were previously stored
if param == nil {
return app.Dao().SaveParam(models.ParamAppSettings, app.settings, encryptionKey)
if storedSettings == nil {
return app.Dao().SaveSettings(app.settings, encryptionKey)
}

// load the settings from the stored param into the app ones
// ---
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
}

afterMergeRaw, err := json.Marshal(app.settings)
if err != nil {
if err := app.settings.Merge(storedSettings); 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
}

// -------------------------------------------------------------------
// 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] {
return app.onBeforeServe
}
Expand Down
19 changes: 3 additions & 16 deletions core/settings_refresh_test.go → core/base_settings_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package core_test

import (
"bytes"
"testing"

"github.com/pocketbase/pocketbase/models"
Expand Down Expand Up @@ -32,7 +31,7 @@ func TestBaseAppRefreshSettings(t *testing.T) {
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}`))
if err := app.Dao().SaveParam(param.Key, param.Value); err != nil {
t.Fatalf("Failed to update the test settings: %v", err)
Expand All @@ -41,21 +40,9 @@ func TestBaseAppRefreshSettings(t *testing.T) {
if err := app.RefreshSettings(); err != nil {
t.Fatalf("Failed to refresh the app settings: %v", err)
}
testEventCalls(t, app, map[string]int{
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
})

// make sure that the newly merged settings were actually saved
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))
}
testEventCalls(t, app, nil)

// try to refresh again and ensure that there was no db update
// try to refresh again without doing any changes
app.ResetEventCalls()
if err := app.RefreshSettings(); err != nil {
t.Fatalf("Failed to refresh the app settings without change: %v", err)
Expand Down
11 changes: 8 additions & 3 deletions core/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tools/mailer"
"github.com/pocketbase/pocketbase/tools/search"
"github.com/pocketbase/pocketbase/tools/subscriptions"
Expand All @@ -15,6 +16,10 @@ import (
// Serve events data
// -------------------------------------------------------------------

type BootstrapEvent struct {
App App
}

type ServeEvent struct {
App App
Router *echo.Echo
Expand Down Expand Up @@ -68,13 +73,13 @@ type RealtimeSubscribeEvent struct {

type SettingsListEvent struct {
HttpContext echo.Context
RedactedSettings *Settings
RedactedSettings *settings.Settings
}

type SettingsUpdateEvent struct {
HttpContext echo.Context
OldSettings *Settings
NewSettings *Settings
OldSettings *settings.Settings
NewSettings *settings.Settings
}

// -------------------------------------------------------------------
Expand Down
63 changes: 63 additions & 0 deletions daos/settings.go
Original file line number Diff line number Diff line change
@@ -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...)
}
Loading

0 comments on commit 8c9b657

Please sign in to comment.