moved settings under models and added settings dao helpers
This commit is contained in:
+10
-1
@@ -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"
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
|
||||
+37
-60
@@ -1,10 +1,8 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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]
|
||||
@@ -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]{},
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
if err := app.settings.Merge(storedSettings); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package core_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -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,
|
||||
})
|
||||
testEventCalls(t, app, nil)
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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)
|
||||
+8
-3
@@ -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"
|
||||
@@ -15,6 +16,10 @@ import (
|
||||
// Serve events data
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type BootstrapEvent struct {
|
||||
App App
|
||||
}
|
||||
|
||||
type ServeEvent struct {
|
||||
App App
|
||||
Router *echo.Echo
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -1,490 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/pocketbase/pocketbase/tools/auth"
|
||||
"github.com/pocketbase/pocketbase/tools/rest"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
// Settings defines common app configuration options.
|
||||
type Settings struct {
|
||||
mux sync.RWMutex
|
||||
|
||||
Meta MetaConfig `form:"meta" json:"meta"`
|
||||
Logs LogsConfig `form:"logs" json:"logs"`
|
||||
Smtp SmtpConfig `form:"smtp" json:"smtp"`
|
||||
S3 S3Config `form:"s3" json:"s3"`
|
||||
|
||||
AdminAuthToken TokenConfig `form:"adminAuthToken" json:"adminAuthToken"`
|
||||
AdminPasswordResetToken TokenConfig `form:"adminPasswordResetToken" json:"adminPasswordResetToken"`
|
||||
RecordAuthToken TokenConfig `form:"recordAuthToken" json:"recordAuthToken"`
|
||||
RecordPasswordResetToken TokenConfig `form:"recordPasswordResetToken" json:"recordPasswordResetToken"`
|
||||
RecordEmailChangeToken TokenConfig `form:"recordEmailChangeToken" json:"recordEmailChangeToken"`
|
||||
RecordVerificationToken TokenConfig `form:"recordVerificationToken" json:"recordVerificationToken"`
|
||||
|
||||
// Deprecated: Will be removed in v0.9!
|
||||
EmailAuth EmailAuthConfig `form:"emailAuth" json:"emailAuth"`
|
||||
|
||||
GoogleAuth AuthProviderConfig `form:"googleAuth" json:"googleAuth"`
|
||||
FacebookAuth AuthProviderConfig `form:"facebookAuth" json:"facebookAuth"`
|
||||
GithubAuth AuthProviderConfig `form:"githubAuth" json:"githubAuth"`
|
||||
GitlabAuth AuthProviderConfig `form:"gitlabAuth" json:"gitlabAuth"`
|
||||
DiscordAuth AuthProviderConfig `form:"discordAuth" json:"discordAuth"`
|
||||
TwitterAuth AuthProviderConfig `form:"twitterAuth" json:"twitterAuth"`
|
||||
MicrosoftAuth AuthProviderConfig `form:"microsoftAuth" json:"microsoftAuth"`
|
||||
SpotifyAuth AuthProviderConfig `form:"spotifyAuth" json:"spotifyAuth"`
|
||||
KakaoAuth AuthProviderConfig `form:"kakaoAuth" json:"kakaoAuth"`
|
||||
TwitchAuth AuthProviderConfig `form:"twitchAuth" json:"twitchAuth"`
|
||||
}
|
||||
|
||||
// NewSettings creates and returns a new default Settings instance.
|
||||
func NewSettings() *Settings {
|
||||
return &Settings{
|
||||
Meta: MetaConfig{
|
||||
AppName: "Acme",
|
||||
AppUrl: "http://localhost:8090",
|
||||
HideControls: false,
|
||||
SenderName: "Support",
|
||||
SenderAddress: "support@example.com",
|
||||
VerificationTemplate: defaultVerificationTemplate,
|
||||
ResetPasswordTemplate: defaultResetPasswordTemplate,
|
||||
ConfirmEmailChangeTemplate: defaultConfirmEmailChangeTemplate,
|
||||
},
|
||||
Logs: LogsConfig{
|
||||
MaxDays: 5,
|
||||
},
|
||||
Smtp: SmtpConfig{
|
||||
Enabled: false,
|
||||
Host: "smtp.example.com",
|
||||
Port: 587,
|
||||
Username: "",
|
||||
Password: "",
|
||||
Tls: false,
|
||||
},
|
||||
AdminAuthToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 1209600, // 14 days,
|
||||
},
|
||||
AdminPasswordResetToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 1800, // 30 minutes,
|
||||
},
|
||||
RecordAuthToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 1209600, // 14 days,
|
||||
},
|
||||
RecordPasswordResetToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 1800, // 30 minutes,
|
||||
},
|
||||
RecordVerificationToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 604800, // 7 days,
|
||||
},
|
||||
RecordEmailChangeToken: TokenConfig{
|
||||
Secret: security.RandomString(50),
|
||||
Duration: 1800, // 30 minutes,
|
||||
},
|
||||
GoogleAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
FacebookAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
GithubAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
GitlabAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
DiscordAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
TwitterAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
MicrosoftAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
SpotifyAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
KakaoAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
TwitchAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Validate makes Settings validatable by implementing [validation.Validatable] interface.
|
||||
func (s *Settings) Validate() error {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
return validation.ValidateStruct(s,
|
||||
validation.Field(&s.Meta),
|
||||
validation.Field(&s.Logs),
|
||||
validation.Field(&s.AdminAuthToken),
|
||||
validation.Field(&s.AdminPasswordResetToken),
|
||||
validation.Field(&s.RecordAuthToken),
|
||||
validation.Field(&s.RecordPasswordResetToken),
|
||||
validation.Field(&s.RecordEmailChangeToken),
|
||||
validation.Field(&s.RecordVerificationToken),
|
||||
validation.Field(&s.Smtp),
|
||||
validation.Field(&s.S3),
|
||||
validation.Field(&s.GoogleAuth),
|
||||
validation.Field(&s.FacebookAuth),
|
||||
validation.Field(&s.GithubAuth),
|
||||
validation.Field(&s.GitlabAuth),
|
||||
validation.Field(&s.DiscordAuth),
|
||||
validation.Field(&s.TwitterAuth),
|
||||
validation.Field(&s.MicrosoftAuth),
|
||||
validation.Field(&s.SpotifyAuth),
|
||||
validation.Field(&s.KakaoAuth),
|
||||
validation.Field(&s.TwitchAuth),
|
||||
)
|
||||
}
|
||||
|
||||
// Merge merges `other` settings into the current one.
|
||||
func (s *Settings) Merge(other *Settings) error {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
bytes, err := json.Marshal(other)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(bytes, s)
|
||||
}
|
||||
|
||||
// Clone creates a new deep copy of the current settings.
|
||||
func (s *Settings) Clone() (*Settings, error) {
|
||||
settings := &Settings{}
|
||||
if err := settings.Merge(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// RedactClone creates a new deep copy of the current settings,
|
||||
// while replacing the secret values with `******`.
|
||||
func (s *Settings) RedactClone() (*Settings, error) {
|
||||
clone, err := s.Clone()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mask := "******"
|
||||
|
||||
sensitiveFields := []*string{
|
||||
&clone.Smtp.Password,
|
||||
&clone.S3.Secret,
|
||||
&clone.AdminAuthToken.Secret,
|
||||
&clone.AdminPasswordResetToken.Secret,
|
||||
&clone.RecordAuthToken.Secret,
|
||||
&clone.RecordPasswordResetToken.Secret,
|
||||
&clone.RecordEmailChangeToken.Secret,
|
||||
&clone.RecordVerificationToken.Secret,
|
||||
&clone.GoogleAuth.ClientSecret,
|
||||
&clone.FacebookAuth.ClientSecret,
|
||||
&clone.GithubAuth.ClientSecret,
|
||||
&clone.GitlabAuth.ClientSecret,
|
||||
&clone.DiscordAuth.ClientSecret,
|
||||
&clone.TwitterAuth.ClientSecret,
|
||||
&clone.MicrosoftAuth.ClientSecret,
|
||||
&clone.SpotifyAuth.ClientSecret,
|
||||
&clone.KakaoAuth.ClientSecret,
|
||||
&clone.TwitchAuth.ClientSecret,
|
||||
}
|
||||
|
||||
// mask all sensitive fields
|
||||
for _, v := range sensitiveFields {
|
||||
if v != nil && *v != "" {
|
||||
*v = mask
|
||||
}
|
||||
}
|
||||
|
||||
return clone, nil
|
||||
}
|
||||
|
||||
// NamedAuthProviderConfigs returns a map with all registered OAuth2
|
||||
// provider configurations (indexed by their name identifier).
|
||||
func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
return map[string]AuthProviderConfig{
|
||||
auth.NameGoogle: s.GoogleAuth,
|
||||
auth.NameFacebook: s.FacebookAuth,
|
||||
auth.NameGithub: s.GithubAuth,
|
||||
auth.NameGitlab: s.GitlabAuth,
|
||||
auth.NameDiscord: s.DiscordAuth,
|
||||
auth.NameTwitter: s.TwitterAuth,
|
||||
auth.NameMicrosoft: s.MicrosoftAuth,
|
||||
auth.NameSpotify: s.SpotifyAuth,
|
||||
auth.NameKakao: s.KakaoAuth,
|
||||
auth.NameTwitch: s.TwitchAuth,
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type TokenConfig struct {
|
||||
Secret string `form:"secret" json:"secret"`
|
||||
Duration int64 `form:"duration" json:"duration"`
|
||||
}
|
||||
|
||||
// Validate makes TokenConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c TokenConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Secret, validation.Required, validation.Length(30, 300)),
|
||||
validation.Field(&c.Duration, validation.Required, validation.Min(5), validation.Max(63072000)),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type SmtpConfig struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
Host string `form:"host" json:"host"`
|
||||
Port int `form:"port" json:"port"`
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
|
||||
// Whether to enforce TLS encryption for the mail server connection.
|
||||
//
|
||||
// When set to false StartTLS command is send, leaving the server
|
||||
// to decide whether to upgrade the connection or not.
|
||||
Tls bool `form:"tls" json:"tls"`
|
||||
}
|
||||
|
||||
// Validate makes SmtpConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c SmtpConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Host, is.Host, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.Port, validation.When(c.Enabled, validation.Required), validation.Min(0)),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type S3Config struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
Bucket string `form:"bucket" json:"bucket"`
|
||||
Region string `form:"region" json:"region"`
|
||||
Endpoint string `form:"endpoint" json:"endpoint"`
|
||||
AccessKey string `form:"accessKey" json:"accessKey"`
|
||||
Secret string `form:"secret" json:"secret"`
|
||||
ForcePathStyle bool `form:"forcePathStyle" json:"forcePathStyle"`
|
||||
}
|
||||
|
||||
// Validate makes S3Config validatable by implementing [validation.Validatable] interface.
|
||||
func (c S3Config) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.Endpoint, is.URL, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.Bucket, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.Region, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.AccessKey, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.Secret, validation.When(c.Enabled, validation.Required)),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type MetaConfig struct {
|
||||
AppName string `form:"appName" json:"appName"`
|
||||
AppUrl string `form:"appUrl" json:"appUrl"`
|
||||
HideControls bool `form:"hideControls" json:"hideControls"`
|
||||
SenderName string `form:"senderName" json:"senderName"`
|
||||
SenderAddress string `form:"senderAddress" json:"senderAddress"`
|
||||
VerificationTemplate EmailTemplate `form:"verificationTemplate" json:"verificationTemplate"`
|
||||
ResetPasswordTemplate EmailTemplate `form:"resetPasswordTemplate" json:"resetPasswordTemplate"`
|
||||
ConfirmEmailChangeTemplate EmailTemplate `form:"confirmEmailChangeTemplate" json:"confirmEmailChangeTemplate"`
|
||||
}
|
||||
|
||||
// Validate makes MetaConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c MetaConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)),
|
||||
validation.Field(&c.AppUrl, validation.Required, is.URL),
|
||||
validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)),
|
||||
validation.Field(&c.SenderAddress, is.EmailFormat, validation.Required),
|
||||
validation.Field(&c.VerificationTemplate, validation.Required),
|
||||
validation.Field(&c.ResetPasswordTemplate, validation.Required),
|
||||
validation.Field(&c.ConfirmEmailChangeTemplate, validation.Required),
|
||||
)
|
||||
}
|
||||
|
||||
type EmailTemplate struct {
|
||||
Body string `form:"body" json:"body"`
|
||||
Subject string `form:"subject" json:"subject"`
|
||||
ActionUrl string `form:"actionUrl" json:"actionUrl"`
|
||||
}
|
||||
|
||||
// Validate makes EmailTemplate validatable by implementing [validation.Validatable] interface.
|
||||
func (t EmailTemplate) Validate() error {
|
||||
return validation.ValidateStruct(&t,
|
||||
validation.Field(&t.Subject, validation.Required),
|
||||
validation.Field(
|
||||
&t.Body,
|
||||
validation.Required,
|
||||
validation.By(checkPlaceholderParams(EmailPlaceholderActionUrl)),
|
||||
),
|
||||
validation.Field(
|
||||
&t.ActionUrl,
|
||||
validation.Required,
|
||||
validation.By(checkPlaceholderParams(EmailPlaceholderToken)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func checkPlaceholderParams(params ...string) validation.RuleFunc {
|
||||
return func(value any) error {
|
||||
v, _ := value.(string)
|
||||
|
||||
for _, param := range params {
|
||||
if !strings.Contains(v, param) {
|
||||
return validation.NewError(
|
||||
"validation_missing_required_param",
|
||||
fmt.Sprintf("Missing required parameter %q", param),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve replaces the placeholder parameters in the current email
|
||||
// template and returns its components as ready-to-use strings.
|
||||
func (t EmailTemplate) Resolve(
|
||||
appName string,
|
||||
appUrl,
|
||||
token string,
|
||||
) (subject, body, actionUrl string) {
|
||||
// replace action url placeholder params (if any)
|
||||
actionUrlParams := map[string]string{
|
||||
EmailPlaceholderAppName: appName,
|
||||
EmailPlaceholderAppUrl: appUrl,
|
||||
EmailPlaceholderToken: token,
|
||||
}
|
||||
actionUrl = t.ActionUrl
|
||||
for k, v := range actionUrlParams {
|
||||
actionUrl = strings.ReplaceAll(actionUrl, k, v)
|
||||
}
|
||||
actionUrl, _ = rest.NormalizeUrl(actionUrl)
|
||||
|
||||
// replace body placeholder params (if any)
|
||||
bodyParams := map[string]string{
|
||||
EmailPlaceholderAppName: appName,
|
||||
EmailPlaceholderAppUrl: appUrl,
|
||||
EmailPlaceholderToken: token,
|
||||
EmailPlaceholderActionUrl: actionUrl,
|
||||
}
|
||||
body = t.Body
|
||||
for k, v := range bodyParams {
|
||||
body = strings.ReplaceAll(body, k, v)
|
||||
}
|
||||
|
||||
// replace subject placeholder params (if any)
|
||||
subjectParams := map[string]string{
|
||||
EmailPlaceholderAppName: appName,
|
||||
EmailPlaceholderAppUrl: appUrl,
|
||||
}
|
||||
subject = t.Subject
|
||||
for k, v := range subjectParams {
|
||||
subject = strings.ReplaceAll(subject, k, v)
|
||||
}
|
||||
|
||||
return subject, body, actionUrl
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type LogsConfig struct {
|
||||
MaxDays int `form:"maxDays" json:"maxDays"`
|
||||
}
|
||||
|
||||
// Validate makes LogsConfig validatable by implementing [validation.Validatable] interface.
|
||||
func (c LogsConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.MaxDays, validation.Min(0)),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type AuthProviderConfig struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
ClientId string `form:"clientId" json:"clientId,omitempty"`
|
||||
ClientSecret string `form:"clientSecret" json:"clientSecret,omitempty"`
|
||||
AuthUrl string `form:"authUrl" json:"authUrl,omitempty"`
|
||||
TokenUrl string `form:"tokenUrl" json:"tokenUrl,omitempty"`
|
||||
UserApiUrl string `form:"userApiUrl" json:"userApiUrl,omitempty"`
|
||||
}
|
||||
|
||||
// Validate makes `ProviderConfig` validatable by implementing [validation.Validatable] interface.
|
||||
func (c AuthProviderConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.ClientId, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.ClientSecret, validation.When(c.Enabled, validation.Required)),
|
||||
validation.Field(&c.AuthUrl, is.URL),
|
||||
validation.Field(&c.TokenUrl, is.URL),
|
||||
validation.Field(&c.UserApiUrl, is.URL),
|
||||
)
|
||||
}
|
||||
|
||||
// SetupProvider loads the current AuthProviderConfig into the specified provider.
|
||||
func (c AuthProviderConfig) SetupProvider(provider auth.Provider) error {
|
||||
if !c.Enabled {
|
||||
return errors.New("The provider is not enabled.")
|
||||
}
|
||||
|
||||
if c.ClientId != "" {
|
||||
provider.SetClientId(c.ClientId)
|
||||
}
|
||||
|
||||
if c.ClientSecret != "" {
|
||||
provider.SetClientSecret(c.ClientSecret)
|
||||
}
|
||||
|
||||
if c.AuthUrl != "" {
|
||||
provider.SetAuthUrl(c.AuthUrl)
|
||||
}
|
||||
|
||||
if c.UserApiUrl != "" {
|
||||
provider.SetUserApiUrl(c.UserApiUrl)
|
||||
}
|
||||
|
||||
if c.TokenUrl != "" {
|
||||
provider.SetTokenUrl(c.TokenUrl)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Deprecated: Will be removed in v0.9!
|
||||
type EmailAuthConfig struct {
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"`
|
||||
OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"`
|
||||
MinPasswordLength int `form:"minPasswordLength" json:"minPasswordLength"`
|
||||
}
|
||||
|
||||
// Deprecated: Will be removed in v0.9!
|
||||
func (c EmailAuthConfig) Validate() error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package core
|
||||
|
||||
// Common settings placeholder tokens
|
||||
const (
|
||||
EmailPlaceholderAppName string = "{APP_NAME}"
|
||||
EmailPlaceholderAppUrl string = "{APP_URL}"
|
||||
EmailPlaceholderToken string = "{TOKEN}"
|
||||
EmailPlaceholderActionUrl string = "{ACTION_URL}"
|
||||
)
|
||||
|
||||
var defaultVerificationTemplate = EmailTemplate{
|
||||
Subject: "Verify your " + EmailPlaceholderAppName + " email",
|
||||
Body: `<p>Hello,</p>
|
||||
<p>Thank you for joining us at ` + EmailPlaceholderAppName + `.</p>
|
||||
<p>Click on the button below to verify your email address.</p>
|
||||
<p>
|
||||
<a class="btn" href="` + EmailPlaceholderActionUrl + `" target="_blank" rel="noopener">Verify</a>
|
||||
</p>
|
||||
<p>
|
||||
Thanks,<br/>
|
||||
` + EmailPlaceholderAppName + ` team
|
||||
</p>`,
|
||||
ActionUrl: EmailPlaceholderAppUrl + "/_/#/auth/confirm-verification/" + EmailPlaceholderToken,
|
||||
}
|
||||
|
||||
var defaultResetPasswordTemplate = EmailTemplate{
|
||||
Subject: "Reset your " + EmailPlaceholderAppName + " password",
|
||||
Body: `<p>Hello,</p>
|
||||
<p>Click on the button below to reset your password.</p>
|
||||
<p>
|
||||
<a class="btn" href="` + EmailPlaceholderActionUrl + `" target="_blank" rel="noopener">Reset password</a>
|
||||
</p>
|
||||
<p><i>If you didn't ask to reset your password, you can ignore this email.</i></p>
|
||||
<p>
|
||||
Thanks,<br/>
|
||||
` + EmailPlaceholderAppName + ` team
|
||||
</p>`,
|
||||
ActionUrl: EmailPlaceholderAppUrl + "/_/#/auth/confirm-password-reset/" + EmailPlaceholderToken,
|
||||
}
|
||||
|
||||
var defaultConfirmEmailChangeTemplate = EmailTemplate{
|
||||
Subject: "Confirm your " + EmailPlaceholderAppName + " new email address",
|
||||
Body: `<p>Hello,</p>
|
||||
<p>Click on the button below to confirm your new email address.</p>
|
||||
<p>
|
||||
<a class="btn" href="` + EmailPlaceholderActionUrl + `" target="_blank" rel="noopener">Confirm new email</a>
|
||||
</p>
|
||||
<p><i>If you didn't ask to change your email address, you can ignore this email.</i></p>
|
||||
<p>
|
||||
Thanks,<br/>
|
||||
` + EmailPlaceholderAppName + ` team
|
||||
</p>`,
|
||||
ActionUrl: EmailPlaceholderAppUrl + "/_/#/auth/confirm-email-change/" + EmailPlaceholderToken,
|
||||
}
|
||||
@@ -1,761 +0,0 @@
|
||||
package core_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/auth"
|
||||
)
|
||||
|
||||
func TestSettingsValidate(t *testing.T) {
|
||||
s := core.NewSettings()
|
||||
|
||||
// set invalid settings data
|
||||
s.Meta.AppName = ""
|
||||
s.Logs.MaxDays = -10
|
||||
s.Smtp.Enabled = true
|
||||
s.Smtp.Host = ""
|
||||
s.S3.Enabled = true
|
||||
s.S3.Endpoint = "invalid"
|
||||
s.AdminAuthToken.Duration = -10
|
||||
s.AdminPasswordResetToken.Duration = -10
|
||||
s.RecordAuthToken.Duration = -10
|
||||
s.RecordPasswordResetToken.Duration = -10
|
||||
s.RecordEmailChangeToken.Duration = -10
|
||||
s.RecordVerificationToken.Duration = -10
|
||||
s.GoogleAuth.Enabled = true
|
||||
s.GoogleAuth.ClientId = ""
|
||||
s.FacebookAuth.Enabled = true
|
||||
s.FacebookAuth.ClientId = ""
|
||||
s.GithubAuth.Enabled = true
|
||||
s.GithubAuth.ClientId = ""
|
||||
s.GitlabAuth.Enabled = true
|
||||
s.GitlabAuth.ClientId = ""
|
||||
s.DiscordAuth.Enabled = true
|
||||
s.DiscordAuth.ClientId = ""
|
||||
s.TwitterAuth.Enabled = true
|
||||
s.TwitterAuth.ClientId = ""
|
||||
s.MicrosoftAuth.Enabled = true
|
||||
s.MicrosoftAuth.ClientId = ""
|
||||
s.SpotifyAuth.Enabled = true
|
||||
s.SpotifyAuth.ClientId = ""
|
||||
s.KakaoAuth.Enabled = true
|
||||
s.KakaoAuth.ClientId = ""
|
||||
s.TwitchAuth.Enabled = true
|
||||
s.TwitchAuth.ClientId = ""
|
||||
|
||||
// check if Validate() is triggering the members validate methods.
|
||||
err := s.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error, got nil")
|
||||
}
|
||||
|
||||
expectations := []string{
|
||||
`"meta":{`,
|
||||
`"logs":{`,
|
||||
`"smtp":{`,
|
||||
`"s3":{`,
|
||||
`"adminAuthToken":{`,
|
||||
`"adminPasswordResetToken":{`,
|
||||
`"recordAuthToken":{`,
|
||||
`"recordPasswordResetToken":{`,
|
||||
`"recordEmailChangeToken":{`,
|
||||
`"recordVerificationToken":{`,
|
||||
`"googleAuth":{`,
|
||||
`"facebookAuth":{`,
|
||||
`"githubAuth":{`,
|
||||
`"gitlabAuth":{`,
|
||||
`"discordAuth":{`,
|
||||
`"twitterAuth":{`,
|
||||
`"microsoftAuth":{`,
|
||||
`"spotifyAuth":{`,
|
||||
`"kakaoAuth":{`,
|
||||
`"twitchAuth":{`,
|
||||
}
|
||||
|
||||
errBytes, _ := json.Marshal(err)
|
||||
jsonErr := string(errBytes)
|
||||
for _, expected := range expectations {
|
||||
if !strings.Contains(jsonErr, expected) {
|
||||
t.Errorf("Expected error key %s in %v", expected, jsonErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsMerge(t *testing.T) {
|
||||
s1 := core.NewSettings()
|
||||
s1.Meta.AppUrl = "old_app_url"
|
||||
|
||||
s2 := core.NewSettings()
|
||||
s2.Meta.AppName = "test"
|
||||
s2.Logs.MaxDays = 123
|
||||
s2.Smtp.Host = "test"
|
||||
s2.Smtp.Enabled = true
|
||||
s2.S3.Enabled = true
|
||||
s2.S3.Endpoint = "test"
|
||||
s2.AdminAuthToken.Duration = 1
|
||||
s2.AdminPasswordResetToken.Duration = 2
|
||||
s2.RecordAuthToken.Duration = 3
|
||||
s2.RecordPasswordResetToken.Duration = 4
|
||||
s2.RecordEmailChangeToken.Duration = 5
|
||||
s2.RecordVerificationToken.Duration = 6
|
||||
s2.GoogleAuth.Enabled = true
|
||||
s2.GoogleAuth.ClientId = "google_test"
|
||||
s2.FacebookAuth.Enabled = true
|
||||
s2.FacebookAuth.ClientId = "facebook_test"
|
||||
s2.GithubAuth.Enabled = true
|
||||
s2.GithubAuth.ClientId = "github_test"
|
||||
s2.GitlabAuth.Enabled = true
|
||||
s2.GitlabAuth.ClientId = "gitlab_test"
|
||||
s2.DiscordAuth.Enabled = true
|
||||
s2.DiscordAuth.ClientId = "discord_test"
|
||||
s2.TwitterAuth.Enabled = true
|
||||
s2.TwitterAuth.ClientId = "twitter_test"
|
||||
s2.MicrosoftAuth.Enabled = true
|
||||
s2.MicrosoftAuth.ClientId = "microsoft_test"
|
||||
s2.SpotifyAuth.Enabled = true
|
||||
s2.SpotifyAuth.ClientId = "spotify_test"
|
||||
s2.KakaoAuth.Enabled = true
|
||||
s2.KakaoAuth.ClientId = "kakao_test"
|
||||
s2.TwitchAuth.Enabled = true
|
||||
s2.TwitchAuth.ClientId = "twitch_test"
|
||||
|
||||
if err := s1.Merge(s2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s1Encoded, err := json.Marshal(s1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s2Encoded, err := json.Marshal(s2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(s1Encoded) != string(s2Encoded) {
|
||||
t.Fatalf("Expected the same serialization, got %v VS %v", string(s1Encoded), string(s2Encoded))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsClone(t *testing.T) {
|
||||
s1 := core.NewSettings()
|
||||
|
||||
s2, err := s1.Clone()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s1Bytes, err := json.Marshal(s1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s2Bytes, err := json.Marshal(s2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(s1Bytes) != string(s2Bytes) {
|
||||
t.Fatalf("Expected equivalent serialization, got %v VS %v", string(s1Bytes), string(s2Bytes))
|
||||
}
|
||||
|
||||
// verify that it is a deep copy
|
||||
s1.Meta.AppName = "new"
|
||||
if s1.Meta.AppName == s2.Meta.AppName {
|
||||
t.Fatalf("Expected s1 and s2 to have different Meta.AppName, got %s", s1.Meta.AppName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsRedactClone(t *testing.T) {
|
||||
s1 := core.NewSettings()
|
||||
s1.Meta.AppName = "test123" // control field
|
||||
s1.Smtp.Password = "test123"
|
||||
s1.Smtp.Tls = true
|
||||
s1.S3.Secret = "test123"
|
||||
s1.AdminAuthToken.Secret = "test123"
|
||||
s1.AdminPasswordResetToken.Secret = "test123"
|
||||
s1.RecordAuthToken.Secret = "test123"
|
||||
s1.RecordPasswordResetToken.Secret = "test123"
|
||||
s1.RecordEmailChangeToken.Secret = "test123"
|
||||
s1.RecordVerificationToken.Secret = "test123"
|
||||
s1.GoogleAuth.ClientSecret = "test123"
|
||||
s1.FacebookAuth.ClientSecret = "test123"
|
||||
s1.GithubAuth.ClientSecret = "test123"
|
||||
s1.GitlabAuth.ClientSecret = "test123"
|
||||
s1.DiscordAuth.ClientSecret = "test123"
|
||||
s1.TwitterAuth.ClientSecret = "test123"
|
||||
s1.MicrosoftAuth.ClientSecret = "test123"
|
||||
s1.SpotifyAuth.ClientSecret = "test123"
|
||||
s1.KakaoAuth.ClientSecret = "test123"
|
||||
s1.TwitchAuth.ClientSecret = "test123"
|
||||
|
||||
s2, err := s1.RedactClone()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(s2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","hideControls":false,"senderName":"Support","senderAddress":"support@example.com","verificationTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eThank you for joining us at {APP_NAME}.\u003c/p\u003e\n\u003cp\u003eClick on the button below to verify your email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eVerify\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Verify your {APP_NAME} email","actionUrl":"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}"},"resetPasswordTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to reset your password.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eReset password\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to reset your password, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Reset your {APP_NAME} password","actionUrl":"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}"},"confirmEmailChangeTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to confirm your new email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eConfirm new email\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to change your email address, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Confirm your {APP_NAME} new email address","actionUrl":"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":5},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"recordAuthToken":{"secret":"******","duration":1209600},"recordPasswordResetToken":{"secret":"******","duration":1800},"recordEmailChangeToken":{"secret":"******","duration":1800},"recordVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":false,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":0},"googleAuth":{"enabled":false,"clientSecret":"******"},"facebookAuth":{"enabled":false,"clientSecret":"******"},"githubAuth":{"enabled":false,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"clientSecret":"******"},"discordAuth":{"enabled":false,"clientSecret":"******"},"twitterAuth":{"enabled":false,"clientSecret":"******"},"microsoftAuth":{"enabled":false,"clientSecret":"******"},"spotifyAuth":{"enabled":false,"clientSecret":"******"},"kakaoAuth":{"enabled":false,"clientSecret":"******"},"twitchAuth":{"enabled":false,"clientSecret":"******"}}`
|
||||
|
||||
if encodedStr := string(encoded); encodedStr != expected {
|
||||
t.Fatalf("Expected\n%v\ngot\n%v", expected, encodedStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedAuthProviderConfigs(t *testing.T) {
|
||||
s := core.NewSettings()
|
||||
|
||||
s.GoogleAuth.ClientId = "google_test"
|
||||
s.FacebookAuth.ClientId = "facebook_test"
|
||||
s.GithubAuth.ClientId = "github_test"
|
||||
s.GitlabAuth.ClientId = "gitlab_test"
|
||||
s.GitlabAuth.Enabled = true
|
||||
s.DiscordAuth.ClientId = "discord_test"
|
||||
s.TwitterAuth.ClientId = "twitter_test"
|
||||
s.MicrosoftAuth.ClientId = "microsoft_test"
|
||||
s.SpotifyAuth.ClientId = "spotify_test"
|
||||
s.KakaoAuth.ClientId = "kakao_test"
|
||||
s.TwitchAuth.ClientId = "twitch_test"
|
||||
|
||||
result := s.NamedAuthProviderConfigs()
|
||||
|
||||
encoded, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
encodedStr := string(encoded)
|
||||
|
||||
expectedParts := []string{
|
||||
`"discord":{"enabled":false,"clientId":"discord_test"}`,
|
||||
`"facebook":{"enabled":false,"clientId":"facebook_test"}`,
|
||||
`"github":{"enabled":false,"clientId":"github_test"}`,
|
||||
`"gitlab":{"enabled":true,"clientId":"gitlab_test"}`,
|
||||
`"google":{"enabled":false,"clientId":"google_test"}`,
|
||||
`"microsoft":{"enabled":false,"clientId":"microsoft_test"}`,
|
||||
`"spotify":{"enabled":false,"clientId":"spotify_test"}`,
|
||||
`"twitter":{"enabled":false,"clientId":"twitter_test"}`,
|
||||
`"kakao":{"enabled":false,"clientId":"kakao_test"}`,
|
||||
`"twitch":{"enabled":false,"clientId":"twitch_test"}`,
|
||||
}
|
||||
for _, p := range expectedParts {
|
||||
if !strings.Contains(encodedStr, p) {
|
||||
t.Fatalf("Expected \n%s \nin \n%s", p, encodedStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenConfigValidate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
config core.TokenConfig
|
||||
expectError bool
|
||||
}{
|
||||
// zero values
|
||||
{
|
||||
core.TokenConfig{},
|
||||
true,
|
||||
},
|
||||
// invalid data
|
||||
{
|
||||
core.TokenConfig{
|
||||
Secret: strings.Repeat("a", 5),
|
||||
Duration: 4,
|
||||
},
|
||||
true,
|
||||
},
|
||||
// valid secret but invalid duration
|
||||
{
|
||||
core.TokenConfig{
|
||||
Secret: strings.Repeat("a", 30),
|
||||
Duration: 63072000 + 1,
|
||||
},
|
||||
true,
|
||||
},
|
||||
// valid data
|
||||
{
|
||||
core.TokenConfig{
|
||||
Secret: strings.Repeat("a", 30),
|
||||
Duration: 100,
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
result := scenario.config.Validate()
|
||||
|
||||
if result != nil && !scenario.expectError {
|
||||
t.Errorf("(%d) Didn't expect error, got %v", i, result)
|
||||
}
|
||||
|
||||
if result == nil && scenario.expectError {
|
||||
t.Errorf("(%d) Expected error, got nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmtpConfigValidate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
config core.SmtpConfig
|
||||
expectError bool
|
||||
}{
|
||||
// zero values (disabled)
|
||||
{
|
||||
core.SmtpConfig{},
|
||||
false,
|
||||
},
|
||||
// zero values (enabled)
|
||||
{
|
||||
core.SmtpConfig{Enabled: true},
|
||||
true,
|
||||
},
|
||||
// invalid data
|
||||
{
|
||||
core.SmtpConfig{
|
||||
Enabled: true,
|
||||
Host: "test:test:test",
|
||||
Port: -10,
|
||||
},
|
||||
true,
|
||||
},
|
||||
// valid data
|
||||
{
|
||||
core.SmtpConfig{
|
||||
Enabled: true,
|
||||
Host: "example.com",
|
||||
Port: 100,
|
||||
Tls: true,
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
result := scenario.config.Validate()
|
||||
|
||||
if result != nil && !scenario.expectError {
|
||||
t.Errorf("(%d) Didn't expect error, got %v", i, result)
|
||||
}
|
||||
|
||||
if result == nil && scenario.expectError {
|
||||
t.Errorf("(%d) Expected error, got nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ConfigValidate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
config core.S3Config
|
||||
expectError bool
|
||||
}{
|
||||
// zero values (disabled)
|
||||
{
|
||||
core.S3Config{},
|
||||
false,
|
||||
},
|
||||
// zero values (enabled)
|
||||
{
|
||||
core.S3Config{Enabled: true},
|
||||
true,
|
||||
},
|
||||
// invalid data
|
||||
{
|
||||
core.S3Config{
|
||||
Enabled: true,
|
||||
Endpoint: "test:test:test",
|
||||
},
|
||||
true,
|
||||
},
|
||||
// valid data (url endpoint)
|
||||
{
|
||||
core.S3Config{
|
||||
Enabled: true,
|
||||
Endpoint: "https://localhost:8090",
|
||||
Bucket: "test",
|
||||
Region: "test",
|
||||
AccessKey: "test",
|
||||
Secret: "test",
|
||||
},
|
||||
false,
|
||||
},
|
||||
// valid data (hostname endpoint)
|
||||
{
|
||||
core.S3Config{
|
||||
Enabled: true,
|
||||
Endpoint: "example.com",
|
||||
Bucket: "test",
|
||||
Region: "test",
|
||||
AccessKey: "test",
|
||||
Secret: "test",
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
result := scenario.config.Validate()
|
||||
|
||||
if result != nil && !scenario.expectError {
|
||||
t.Errorf("(%d) Didn't expect error, got %v", i, result)
|
||||
}
|
||||
|
||||
if result == nil && scenario.expectError {
|
||||
t.Errorf("(%d) Expected error, got nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaConfigValidate(t *testing.T) {
|
||||
invalidTemplate := core.EmailTemplate{
|
||||
Subject: "test",
|
||||
ActionUrl: "test",
|
||||
Body: "test",
|
||||
}
|
||||
|
||||
noPlaceholdersTemplate := core.EmailTemplate{
|
||||
Subject: "test",
|
||||
ActionUrl: "http://example.com",
|
||||
Body: "test",
|
||||
}
|
||||
|
||||
withPlaceholdersTemplate := core.EmailTemplate{
|
||||
Subject: "test",
|
||||
ActionUrl: "http://example.com" + core.EmailPlaceholderToken,
|
||||
Body: "test" + core.EmailPlaceholderActionUrl,
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
config core.MetaConfig
|
||||
expectError bool
|
||||
}{
|
||||
// zero values
|
||||
{
|
||||
core.MetaConfig{},
|
||||
true,
|
||||
},
|
||||
// invalid data
|
||||
{
|
||||
core.MetaConfig{
|
||||
AppName: strings.Repeat("a", 300),
|
||||
AppUrl: "test",
|
||||
SenderName: strings.Repeat("a", 300),
|
||||
SenderAddress: "invalid_email",
|
||||
VerificationTemplate: invalidTemplate,
|
||||
ResetPasswordTemplate: invalidTemplate,
|
||||
ConfirmEmailChangeTemplate: invalidTemplate,
|
||||
},
|
||||
true,
|
||||
},
|
||||
// invalid data (missing required placeholders)
|
||||
{
|
||||
core.MetaConfig{
|
||||
AppName: "test",
|
||||
AppUrl: "https://example.com",
|
||||
SenderName: "test",
|
||||
SenderAddress: "test@example.com",
|
||||
VerificationTemplate: noPlaceholdersTemplate,
|
||||
ResetPasswordTemplate: noPlaceholdersTemplate,
|
||||
ConfirmEmailChangeTemplate: noPlaceholdersTemplate,
|
||||
},
|
||||
true,
|
||||
},
|
||||
// valid data
|
||||
{
|
||||
core.MetaConfig{
|
||||
AppName: "test",
|
||||
AppUrl: "https://example.com",
|
||||
SenderName: "test",
|
||||
SenderAddress: "test@example.com",
|
||||
VerificationTemplate: withPlaceholdersTemplate,
|
||||
ResetPasswordTemplate: withPlaceholdersTemplate,
|
||||
ConfirmEmailChangeTemplate: withPlaceholdersTemplate,
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
result := scenario.config.Validate()
|
||||
|
||||
if result != nil && !scenario.expectError {
|
||||
t.Errorf("(%d) Didn't expect error, got %v", i, result)
|
||||
}
|
||||
|
||||
if result == nil && scenario.expectError {
|
||||
t.Errorf("(%d) Expected error, got nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailTemplateValidate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
emailTemplate core.EmailTemplate
|
||||
expectedErrors []string
|
||||
}{
|
||||
// require values
|
||||
{
|
||||
core.EmailTemplate{},
|
||||
[]string{"subject", "actionUrl", "body"},
|
||||
},
|
||||
// missing placeholders
|
||||
{
|
||||
core.EmailTemplate{
|
||||
Subject: "test",
|
||||
ActionUrl: "test",
|
||||
Body: "test",
|
||||
},
|
||||
[]string{"actionUrl", "body"},
|
||||
},
|
||||
// valid data
|
||||
{
|
||||
core.EmailTemplate{
|
||||
Subject: "test",
|
||||
ActionUrl: "test" + core.EmailPlaceholderToken,
|
||||
Body: "test" + core.EmailPlaceholderActionUrl,
|
||||
},
|
||||
[]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
result := s.emailTemplate.Validate()
|
||||
|
||||
// parse errors
|
||||
errs, ok := result.(validation.Errors)
|
||||
if !ok && result != nil {
|
||||
t.Errorf("(%d) Failed to parse errors %v", i, result)
|
||||
continue
|
||||
}
|
||||
|
||||
// check errors
|
||||
if len(errs) > len(s.expectedErrors) {
|
||||
t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs)
|
||||
}
|
||||
for _, k := range s.expectedErrors {
|
||||
if _, ok := errs[k]; !ok {
|
||||
t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailTemplateResolve(t *testing.T) {
|
||||
allPlaceholders := core.EmailPlaceholderActionUrl + core.EmailPlaceholderToken + core.EmailPlaceholderAppName + core.EmailPlaceholderAppUrl
|
||||
|
||||
scenarios := []struct {
|
||||
emailTemplate core.EmailTemplate
|
||||
expectedSubject string
|
||||
expectedBody string
|
||||
expectedActionUrl string
|
||||
}{
|
||||
// no placeholders
|
||||
{
|
||||
emailTemplate: core.EmailTemplate{
|
||||
Subject: "subject:",
|
||||
Body: "body:",
|
||||
ActionUrl: "/actionUrl////",
|
||||
},
|
||||
expectedSubject: "subject:",
|
||||
expectedActionUrl: "/actionUrl/",
|
||||
expectedBody: "body:",
|
||||
},
|
||||
// with placeholders
|
||||
{
|
||||
emailTemplate: core.EmailTemplate{
|
||||
ActionUrl: "/actionUrl////" + allPlaceholders,
|
||||
Subject: "subject:" + allPlaceholders,
|
||||
Body: "body:" + allPlaceholders,
|
||||
},
|
||||
expectedActionUrl: fmt.Sprintf(
|
||||
"/actionUrl/%%7BACTION_URL%%7D%s%s%s",
|
||||
"token_test",
|
||||
"name_test",
|
||||
"url_test",
|
||||
),
|
||||
expectedSubject: fmt.Sprintf(
|
||||
"subject:%s%s%s%s",
|
||||
core.EmailPlaceholderActionUrl,
|
||||
core.EmailPlaceholderToken,
|
||||
"name_test",
|
||||
"url_test",
|
||||
),
|
||||
expectedBody: fmt.Sprintf(
|
||||
"body:%s%s%s%s",
|
||||
fmt.Sprintf(
|
||||
"/actionUrl/%%7BACTION_URL%%7D%s%s%s",
|
||||
"token_test",
|
||||
"name_test",
|
||||
"url_test",
|
||||
),
|
||||
"token_test",
|
||||
"name_test",
|
||||
"url_test",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
subject, body, actionUrl := s.emailTemplate.Resolve("name_test", "url_test", "token_test")
|
||||
|
||||
if s.expectedSubject != subject {
|
||||
t.Errorf("(%d) Expected subject %q got %q", i, s.expectedSubject, subject)
|
||||
}
|
||||
|
||||
if s.expectedBody != body {
|
||||
t.Errorf("(%d) Expected body \n%v got \n%v", i, s.expectedBody, body)
|
||||
}
|
||||
|
||||
if s.expectedActionUrl != actionUrl {
|
||||
t.Errorf("(%d) Expected actionUrl \n%v got \n%v", i, s.expectedActionUrl, actionUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogsConfigValidate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
config core.LogsConfig
|
||||
expectError bool
|
||||
}{
|
||||
// zero values
|
||||
{
|
||||
core.LogsConfig{},
|
||||
false,
|
||||
},
|
||||
// invalid data
|
||||
{
|
||||
core.LogsConfig{MaxDays: -10},
|
||||
true,
|
||||
},
|
||||
// valid data
|
||||
{
|
||||
core.LogsConfig{MaxDays: 1},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
result := scenario.config.Validate()
|
||||
|
||||
if result != nil && !scenario.expectError {
|
||||
t.Errorf("(%d) Didn't expect error, got %v", i, result)
|
||||
}
|
||||
|
||||
if result == nil && scenario.expectError {
|
||||
t.Errorf("(%d) Expected error, got nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthProviderConfigValidate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
config core.AuthProviderConfig
|
||||
expectError bool
|
||||
}{
|
||||
// zero values (disabled)
|
||||
{
|
||||
core.AuthProviderConfig{},
|
||||
false,
|
||||
},
|
||||
// zero values (enabled)
|
||||
{
|
||||
core.AuthProviderConfig{Enabled: true},
|
||||
true,
|
||||
},
|
||||
// invalid data
|
||||
{
|
||||
core.AuthProviderConfig{
|
||||
Enabled: true,
|
||||
ClientId: "",
|
||||
ClientSecret: "",
|
||||
AuthUrl: "test",
|
||||
TokenUrl: "test",
|
||||
UserApiUrl: "test",
|
||||
},
|
||||
true,
|
||||
},
|
||||
// valid data (only the required)
|
||||
{
|
||||
core.AuthProviderConfig{
|
||||
Enabled: true,
|
||||
ClientId: "test",
|
||||
ClientSecret: "test",
|
||||
},
|
||||
false,
|
||||
},
|
||||
// valid data (fill all fields)
|
||||
{
|
||||
core.AuthProviderConfig{
|
||||
Enabled: true,
|
||||
ClientId: "test",
|
||||
ClientSecret: "test",
|
||||
AuthUrl: "https://example.com",
|
||||
TokenUrl: "https://example.com",
|
||||
UserApiUrl: "https://example.com",
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
result := scenario.config.Validate()
|
||||
|
||||
if result != nil && !scenario.expectError {
|
||||
t.Errorf("(%d) Didn't expect error, got %v", i, result)
|
||||
}
|
||||
|
||||
if result == nil && scenario.expectError {
|
||||
t.Errorf("(%d) Expected error, got nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthProviderConfigSetupProvider(t *testing.T) {
|
||||
provider := auth.NewGithubProvider()
|
||||
|
||||
// disabled config
|
||||
c1 := core.AuthProviderConfig{Enabled: false}
|
||||
if err := c1.SetupProvider(provider); err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
}
|
||||
|
||||
c2 := core.AuthProviderConfig{
|
||||
Enabled: true,
|
||||
ClientId: "test_ClientId",
|
||||
ClientSecret: "test_ClientSecret",
|
||||
AuthUrl: "test_AuthUrl",
|
||||
UserApiUrl: "test_UserApiUrl",
|
||||
TokenUrl: "test_TokenUrl",
|
||||
}
|
||||
if err := c2.SetupProvider(provider); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if provider.ClientId() != c2.ClientId {
|
||||
t.Fatalf("Expected ClientId %s, got %s", c2.ClientId, provider.ClientId())
|
||||
}
|
||||
|
||||
if provider.ClientSecret() != c2.ClientSecret {
|
||||
t.Fatalf("Expected ClientSecret %s, got %s", c2.ClientSecret, provider.ClientSecret())
|
||||
}
|
||||
|
||||
if provider.AuthUrl() != c2.AuthUrl {
|
||||
t.Fatalf("Expected AuthUrl %s, got %s", c2.AuthUrl, provider.AuthUrl())
|
||||
}
|
||||
|
||||
if provider.UserApiUrl() != c2.UserApiUrl {
|
||||
t.Fatalf("Expected UserApiUrl %s, got %s", c2.UserApiUrl, provider.UserApiUrl())
|
||||
}
|
||||
|
||||
if provider.TokenUrl() != c2.TokenUrl {
|
||||
t.Fatalf("Expected TokenUrl %s, got %s", c2.TokenUrl, provider.TokenUrl())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user