From a426484916d847e17e18263f1033f88481babe7f Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Sun, 7 Aug 2022 15:38:21 +0300 Subject: [PATCH] added WithConfig factory to all forms --- forms/admin_login.go | 22 ++++++----- forms/admin_password_reset_confirm.go | 49 ++++++++++++++++++----- forms/admin_password_reset_request.go | 51 ++++++++++++++++++------ forms/admin_upsert.go | 20 ++++++---- forms/collection_upsert.go | 24 +++++++----- forms/collections_import.go | 49 ++++++++++++----------- forms/realtime_subscribe.go | 2 +- forms/record_upsert.go | 35 ++++++++--------- forms/settings_upsert.go | 52 ++++++++++++++++++++----- forms/user_email_change_confirm.go | 50 +++++++++++++++++++----- forms/user_email_change_request.go | 47 ++++++++++++++++++---- forms/user_email_login.go | 40 ++++++++++++++++--- forms/user_oauth2_login.go | 56 +++++++++++++++++++++------ forms/user_password_reset_confirm.go | 52 +++++++++++++++++++------ forms/user_password_reset_request.go | 52 +++++++++++++++++++------ forms/user_upsert.go | 36 ++++++++--------- forms/user_verification_confirm.go | 50 +++++++++++++++++++----- forms/user_verification_request.go | 50 +++++++++++++++++++----- 18 files changed, 542 insertions(+), 195 deletions(-) diff --git a/forms/admin_login.go b/forms/admin_login.go index fbdad0c4..89c6ff15 100644 --- a/forms/admin_login.go +++ b/forms/admin_login.go @@ -10,7 +10,7 @@ import ( "github.com/pocketbase/pocketbase/models" ) -// AdminLogin defines an admin email/pass login form. +// AdminLogin specifies an admin email/pass login form. type AdminLogin struct { config AdminLoginConfig @@ -20,20 +20,20 @@ type AdminLogin struct { // AdminLoginConfig is the [AdminLogin] factory initializer config. // -// NB! Dao is a required struct member. +// NB! App is a required struct member. type AdminLoginConfig struct { - Dao *daos.Dao + App core.App + TxDao *daos.Dao } // NewAdminLogin creates a new [AdminLogin] form with initializer // config created from the provided [core.App] instance. // -// This factory method is used primarily for convenience (and backward compatibility). // If you want to submit the form as part of another transaction, use -// [NewCollectionUpsertWithConfig] with Dao configured to your txDao. +// [NewAdminLoginWithConfig] with explicitly set TxDao. func NewAdminLogin(app core.App) *AdminLogin { return NewAdminLoginWithConfig(AdminLoginConfig{ - Dao: app.Dao(), + App: app, }) } @@ -42,8 +42,12 @@ func NewAdminLogin(app core.App) *AdminLogin { func NewAdminLoginWithConfig(config AdminLoginConfig) *AdminLogin { form := &AdminLogin{config: config} - if form.config.Dao == nil { - panic("Invalid initializer config.") + if form.config.App == nil { + panic("Missing required config.App instance.") + } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() } return form @@ -64,7 +68,7 @@ func (form *AdminLogin) Submit() (*models.Admin, error) { return nil, err } - admin, err := form.config.Dao.FindAdminByEmail(form.Email) + admin, err := form.config.TxDao.FindAdminByEmail(form.Email) if err != nil { return nil, err } diff --git a/forms/admin_password_reset_confirm.go b/forms/admin_password_reset_confirm.go index 759bc77d..3bc79a77 100644 --- a/forms/admin_password_reset_confirm.go +++ b/forms/admin_password_reset_confirm.go @@ -3,24 +3,53 @@ package forms import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/forms/validators" "github.com/pocketbase/pocketbase/models" ) -// AdminPasswordResetConfirm defines an admin password reset confirmation form. +// AdminPasswordResetConfirm specifies an admin password reset confirmation form. type AdminPasswordResetConfirm struct { - app core.App + config AdminPasswordResetConfirmConfig Token string `form:"token" json:"token"` Password string `form:"password" json:"password"` PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` } -// NewAdminPasswordResetConfirm creates new admin password reset confirmation form. +// AdminPasswordResetConfirmConfig is the [AdminPasswordResetConfirm] factory initializer config. +// +// NB! App is required struct member. +type AdminPasswordResetConfirmConfig struct { + App core.App + TxDao *daos.Dao +} + +// NewAdminPasswordResetConfirm creates a new [AdminPasswordResetConfirm] +// form with initializer config created from the provided [core.App] instance. +// +// If you want to submit the form as part of another transaction, use +// [NewAdminPasswordResetConfirmWithConfig] with explicitly set TxDao. func NewAdminPasswordResetConfirm(app core.App) *AdminPasswordResetConfirm { - return &AdminPasswordResetConfirm{ - app: app, + return NewAdminPasswordResetConfirmWithConfig(AdminPasswordResetConfirmConfig{ + App: app, + }) +} + +// NewAdminPasswordResetConfirmWithConfig creates a new [AdminPasswordResetConfirm] +// form with the provided config or panics on invalid configuration. +func NewAdminPasswordResetConfirmWithConfig(config AdminPasswordResetConfirmConfig) *AdminPasswordResetConfirm { + form := &AdminPasswordResetConfirm{config: config} + + if form.config.App == nil { + panic("Missing required config.App instance.") } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + + return form } // Validate makes the form validatable by implementing [validation.Validatable] interface. @@ -38,9 +67,9 @@ func (form *AdminPasswordResetConfirm) checkToken(value any) error { return nil // nothing to check } - admin, err := form.app.Dao().FindAdminByToken( + admin, err := form.config.TxDao.FindAdminByToken( v, - form.app.Settings().AdminPasswordResetToken.Secret, + form.config.App.Settings().AdminPasswordResetToken.Secret, ) if err != nil || admin == nil { return validation.NewError("validation_invalid_token", "Invalid or expired token.") @@ -56,9 +85,9 @@ func (form *AdminPasswordResetConfirm) Submit() (*models.Admin, error) { return nil, err } - admin, err := form.app.Dao().FindAdminByToken( + admin, err := form.config.TxDao.FindAdminByToken( form.Token, - form.app.Settings().AdminPasswordResetToken.Secret, + form.config.App.Settings().AdminPasswordResetToken.Secret, ) if err != nil { return nil, err @@ -68,7 +97,7 @@ func (form *AdminPasswordResetConfirm) Submit() (*models.Admin, error) { return nil, err } - if err := form.app.Dao().SaveAdmin(admin); err != nil { + if err := form.config.TxDao.SaveAdmin(admin); err != nil { return nil, err } diff --git a/forms/admin_password_reset_request.go b/forms/admin_password_reset_request.go index 283b734e..2653c68c 100644 --- a/forms/admin_password_reset_request.go +++ b/forms/admin_password_reset_request.go @@ -7,24 +7,53 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/mails" "github.com/pocketbase/pocketbase/tools/types" ) -// AdminPasswordResetRequest defines an admin password reset request form. +// AdminPasswordResetRequest specifies an admin password reset request form. type AdminPasswordResetRequest struct { - app core.App - resendThreshold float64 + config AdminPasswordResetRequestConfig Email string `form:"email" json:"email"` } -// NewAdminPasswordResetRequest creates new admin password reset request form. +// AdminPasswordResetRequestConfig is the [AdminPasswordResetRequest] factory initializer config. +// +// NB! App is required struct member. +type AdminPasswordResetRequestConfig struct { + App core.App + TxDao *daos.Dao + ResendThreshold float64 // in seconds +} + +// NewAdminPasswordResetRequest creates a new [AdminPasswordResetRequest] +// form with initializer config created from the provided [core.App] instance. +// +// If you want to submit the form as part of another transaction, use +// [NewAdminPasswordResetRequestWithConfig] with explicitly set TxDao. func NewAdminPasswordResetRequest(app core.App) *AdminPasswordResetRequest { - return &AdminPasswordResetRequest{ - app: app, - resendThreshold: 120, // 2 min + return NewAdminPasswordResetRequestWithConfig(AdminPasswordResetRequestConfig{ + App: app, + ResendThreshold: 120, // 2min + }) +} + +// NewAdminPasswordResetRequestWithConfig creates a new [AdminPasswordResetRequest] +// form with the provided config or panics on invalid configuration. +func NewAdminPasswordResetRequestWithConfig(config AdminPasswordResetRequestConfig) *AdminPasswordResetRequest { + form := &AdminPasswordResetRequest{config: config} + + if form.config.App == nil { + panic("Missing required config.App instance.") } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + + return form } // Validate makes the form validatable by implementing [validation.Validatable] interface. @@ -48,23 +77,23 @@ func (form *AdminPasswordResetRequest) Submit() error { return err } - admin, err := form.app.Dao().FindAdminByEmail(form.Email) + admin, err := form.config.TxDao.FindAdminByEmail(form.Email) if err != nil { return err } now := time.Now().UTC() lastResetSentAt := admin.LastResetSentAt.Time() - if now.Sub(lastResetSentAt).Seconds() < form.resendThreshold { + if now.Sub(lastResetSentAt).Seconds() < form.config.ResendThreshold { return errors.New("You have already requested a password reset.") } - if err := mails.SendAdminPasswordReset(form.app, admin); err != nil { + if err := mails.SendAdminPasswordReset(form.config.App, admin); err != nil { return err } // update last sent timestamp admin.LastResetSentAt = types.NowDateTime() - return form.app.Dao().SaveAdmin(admin) + return form.config.TxDao.SaveAdmin(admin) } diff --git a/forms/admin_upsert.go b/forms/admin_upsert.go index a4690003..85528799 100644 --- a/forms/admin_upsert.go +++ b/forms/admin_upsert.go @@ -23,21 +23,21 @@ type AdminUpsert struct { // AdminUpsertConfig is the [AdminUpsert] factory initializer config. // -// NB! Dao is a required struct member. +// NB! App is a required struct member. type AdminUpsertConfig struct { - Dao *daos.Dao + App core.App + TxDao *daos.Dao } // NewAdminUpsert creates a new [AdminUpsert] form with initializer // config created from the provided [core.App] and [models.Admin] instances // (for create you could pass a pointer to an empty Admin - `&models.Admin{}`). // -// This factory method is used primarily for convenience (and backward compatibility). // If you want to submit the form as part of another transaction, use -// [NewAdminUpsertWithConfig] with Dao configured to your txDao. +// [NewAdminUpsertWithConfig] with explicitly set TxDao. func NewAdminUpsert(app core.App, admin *models.Admin) *AdminUpsert { return NewAdminUpsertWithConfig(AdminUpsertConfig{ - Dao: app.Dao(), + App: app, }, admin) } @@ -50,10 +50,14 @@ func NewAdminUpsertWithConfig(config AdminUpsertConfig, admin *models.Admin) *Ad admin: admin, } - if form.config.Dao == nil || form.admin == nil { + if form.config.App == nil || form.admin == nil { panic("Invalid initializer config or nil upsert model.") } + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + // load defaults form.Id = admin.Id form.Avatar = admin.Avatar @@ -100,7 +104,7 @@ func (form *AdminUpsert) Validate() error { func (form *AdminUpsert) checkUniqueEmail(value any) error { v, _ := value.(string) - if form.config.Dao.IsAdminEmailUnique(v, form.admin.Id) { + if form.config.TxDao.IsAdminEmailUnique(v, form.admin.Id) { return nil } @@ -130,6 +134,6 @@ func (form *AdminUpsert) Submit(interceptors ...InterceptorFunc) error { } return runInterceptors(func() error { - return form.config.Dao.SaveAdmin(form.admin) + return form.config.TxDao.SaveAdmin(form.admin) }, interceptors...) } diff --git a/forms/collection_upsert.go b/forms/collection_upsert.go index e4babdd9..54098f71 100644 --- a/forms/collection_upsert.go +++ b/forms/collection_upsert.go @@ -33,21 +33,21 @@ type CollectionUpsert struct { // CollectionUpsertConfig is the [CollectionUpsert] factory initializer config. // -// NB! Dao is a required struct member. +// NB! App is a required struct member. type CollectionUpsertConfig struct { - Dao *daos.Dao + App core.App + TxDao *daos.Dao } // NewCollectionUpsert creates a new [CollectionUpsert] form with initializer // config created from the provided [core.App] and [models.Collection] instances // (for create you could pass a pointer to an empty Collection - `&models.Collection{}`). // -// This factory method is used primarily for convenience (and backward compatibility). // If you want to submit the form as part of another transaction, use -// [NewCollectionUpsertWithConfig] with Dao configured to your txDao. +// [NewCollectionUpsertWithConfig] with explicitly set TxDao. func NewCollectionUpsert(app core.App, collection *models.Collection) *CollectionUpsert { return NewCollectionUpsertWithConfig(CollectionUpsertConfig{ - Dao: app.Dao(), + App: app, }, collection) } @@ -60,10 +60,14 @@ func NewCollectionUpsertWithConfig(config CollectionUpsertConfig, collection *mo collection: collection, } - if form.config.Dao == nil || form.collection == nil { + if form.config.App == nil || form.collection == nil { panic("Invalid initializer config or nil upsert model.") } + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + // load defaults form.Id = form.collection.Id form.Name = form.collection.Name @@ -123,11 +127,11 @@ func (form *CollectionUpsert) Validate() error { func (form *CollectionUpsert) checkUniqueName(value any) error { v, _ := value.(string) - if !form.config.Dao.IsCollectionNameUnique(v, form.collection.Id) { + if !form.config.TxDao.IsCollectionNameUnique(v, form.collection.Id) { return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).") } - if (form.collection.IsNew() || !strings.EqualFold(v, form.collection.Name)) && form.config.Dao.HasTable(v) { + if (form.collection.IsNew() || !strings.EqualFold(v, form.collection.Name)) && form.config.TxDao.HasTable(v) { return validation.NewError("validation_collection_name_table_exists", "The collection name must be also unique table name.") } @@ -194,7 +198,7 @@ func (form *CollectionUpsert) checkRule(value any) error { } dummy := &models.Collection{Schema: form.Schema} - r := resolvers.NewRecordFieldResolver(form.config.Dao, dummy, nil) + r := resolvers.NewRecordFieldResolver(form.config.TxDao, dummy, nil) _, err := search.FilterData(*v).BuildExpr(r) if err != nil { @@ -239,6 +243,6 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error { form.collection.DeleteRule = form.DeleteRule return runInterceptors(func() error { - return form.config.Dao.SaveCollection(form.collection) + return form.config.TxDao.SaveCollection(form.collection) }, interceptors...) } diff --git a/forms/collections_import.go b/forms/collections_import.go index 4d19e03f..7c3fe301 100644 --- a/forms/collections_import.go +++ b/forms/collections_import.go @@ -22,10 +22,21 @@ type CollectionsImport struct { // CollectionsImportConfig is the [CollectionsImport] factory initializer config. // -// NB! Dao is a required struct member. +// NB! App is a required struct member. type CollectionsImportConfig struct { - Dao *daos.Dao - IsDebug bool + App core.App + TxDao *daos.Dao +} + +// NewCollectionsImport creates a new [CollectionsImport] form with +// initializer config created from the provided [core.App] instance. +// +// If you want to submit the form as part of another transaction, use +// [NewCollectionsImportWithConfig] with explicitly set TxDao. +func NewCollectionsImport(app core.App) *CollectionsImport { + return NewCollectionsImportWithConfig(CollectionsImportConfig{ + App: app, + }) } // NewCollectionsImportWithConfig creates a new [CollectionsImport] @@ -33,26 +44,17 @@ type CollectionsImportConfig struct { func NewCollectionsImportWithConfig(config CollectionsImportConfig) *CollectionsImport { form := &CollectionsImport{config: config} - if form.config.Dao == nil { - panic("Invalid initializer config.") + if form.config.App == nil { + panic("Missing required config.App instance.") + } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() } return form } -// NewCollectionsImport creates a new [CollectionsImport] form with -// initializer config created from the provided [core.App] instance. -// -// This factory method is used primarily for convenience (and backward compatibility). -// If you want to submit the form as part of another transaction, use -// [NewCollectionsImportWithConfig] with Dao configured to your txDao. -func NewCollectionsImport(app core.App) *CollectionsImport { - return NewCollectionsImportWithConfig(CollectionsImportConfig{ - Dao: app.Dao(), - IsDebug: app.IsDebug(), - }) -} - // Validate makes the form validatable by implementing [validation.Validatable] interface. func (form *CollectionsImport) Validate() error { return validation.ValidateStruct(form, @@ -73,7 +75,7 @@ func (form *CollectionsImport) Submit() error { return err } - return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error { + return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error { oldCollections := []*models.Collection{} if err := txDao.CollectionQuery().All(&oldCollections); err != nil { return err @@ -95,7 +97,7 @@ func (form *CollectionsImport) Submit() error { if mappedFormCollections[old.GetId()] == nil { // delete the collection if err := txDao.DeleteCollection(old); err != nil { - if form.config.IsDebug { + if form.config.App.IsDebug() { log.Println("[CollectionsImport] DeleteOthers failure", old.Name, err) } return validation.Errors{"collections": validation.NewError( @@ -117,7 +119,7 @@ func (form *CollectionsImport) Submit() error { } if err := txDao.Save(collection); err != nil { - if form.config.IsDebug { + if form.config.App.IsDebug() { log.Println("[CollectionsImport] Save failure", collection.Name, err) } return validation.Errors{"collections": validation.NewError( @@ -141,7 +143,8 @@ func (form *CollectionsImport) Submit() error { upsertModel = collection } upsertForm := NewCollectionUpsertWithConfig(CollectionUpsertConfig{ - Dao: txDao, + App: form.config.App, + TxDao: txDao, }, upsertModel) // load form fields with the refreshed collection state upsertForm.Id = collection.Id @@ -168,7 +171,7 @@ func (form *CollectionsImport) Submit() error { for _, collection := range form.Collections { oldCollection := mappedOldCollections[collection.GetId()] if err := txDao.SyncRecordTableSchema(collection, oldCollection); err != nil { - if form.config.IsDebug { + if form.config.App.IsDebug() { log.Println("[CollectionsImport] Records table sync failure", collection.Name, err) } return validation.Errors{"collections": validation.NewError( diff --git a/forms/realtime_subscribe.go b/forms/realtime_subscribe.go index 8d3cabcf..a1e83f79 100644 --- a/forms/realtime_subscribe.go +++ b/forms/realtime_subscribe.go @@ -4,7 +4,7 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" ) -// RealtimeSubscribe defines a RealtimeSubscribe request form. +// RealtimeSubscribe specifies a RealtimeSubscribe request form. type RealtimeSubscribe struct { ClientId string `form:"clientId" json:"clientId"` Subscriptions []string `form:"subscriptions" json:"subscriptions"` diff --git a/forms/record_upsert.go b/forms/record_upsert.go index b4a9547e..b864a7fc 100644 --- a/forms/record_upsert.go +++ b/forms/record_upsert.go @@ -16,7 +16,6 @@ import ( "github.com/pocketbase/pocketbase/forms/validators" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models/schema" - "github.com/pocketbase/pocketbase/tools/filesystem" "github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/rest" "github.com/spf13/cast" @@ -36,25 +35,21 @@ type RecordUpsert struct { // RecordUpsertConfig is the [RecordUpsert] factory initializer config. // -// NB! Dao and FilesystemFactory are required struct members. +// NB! App is required struct member. type RecordUpsertConfig struct { - Dao *daos.Dao - FilesystemFactory func() (*filesystem.System, error) - IsDebug bool + App core.App + TxDao *daos.Dao } // NewRecordUpsert creates a new [RecordUpsert] form with initializer // config created from the provided [core.App] and [models.Record] instances // (for create you could pass a pointer to an empty Record - `&models.Record{}`). // -// This factory method is used primarily for convenience (and backward compatibility). // If you want to submit the form as part of another transaction, use -// [NewRecordUpsertWithConfig] with Dao configured to your txDao. +// [NewRecordUpsertWithConfig] with explicitly set TxDao. func NewRecordUpsert(app core.App, record *models.Record) *RecordUpsert { return NewRecordUpsertWithConfig(RecordUpsertConfig{ - Dao: app.Dao(), - FilesystemFactory: app.NewFilesystem, - IsDebug: app.IsDebug(), + App: app, }, record) } @@ -69,12 +64,14 @@ func NewRecordUpsertWithConfig(config RecordUpsertConfig, record *models.Record) filesToUpload: []*rest.UploadedFile{}, } - if form.config.Dao == nil || - form.config.FilesystemFactory == nil || - form.record == nil { + if form.config.App == nil || form.record == nil { panic("Invalid initializer config or nil upsert model.") } + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + form.Id = record.Id form.Data = map[string]any{} @@ -240,7 +237,7 @@ func (form *RecordUpsert) LoadData(r *http.Request) error { // check if there are any new uploaded form files files, err := rest.FindUploadedFiles(r, key) if err != nil { - if form.config.IsDebug { + if form.config.App.IsDebug() { log.Printf("%q uploaded file error: %v\n", key, err) } @@ -288,7 +285,7 @@ func (form *RecordUpsert) Validate() error { // record data validator dataValidator := validators.NewRecordDataValidator( - form.config.Dao, + form.config.TxDao, form.record, form.filesToUpload, ) @@ -316,7 +313,7 @@ func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error return err } - return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error { + return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error { tx, ok := txDao.DB().(*dbx.Tx) if !ok { return errors.New("failed to get transaction db") @@ -356,7 +353,7 @@ func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc) error { } return runInterceptors(func() error { - return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error { + return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error { // persist record model if err := txDao.SaveRecord(form.record); err != nil { return err @@ -387,7 +384,7 @@ func (form *RecordUpsert) processFilesToUpload() error { return errors.New("The record is not persisted yet.") } - fs, err := form.config.FilesystemFactory() + fs, err := form.config.App.NewFilesystem() if err != nil { return err } @@ -423,7 +420,7 @@ func (form *RecordUpsert) processFilesToDelete() error { return errors.New("The record is not persisted yet.") } - fs, err := form.config.FilesystemFactory() + fs, err := form.config.App.NewFilesystem() if err != nil { return err } diff --git a/forms/settings_upsert.go b/forms/settings_upsert.go index ab2543d5..dff96f7a 100644 --- a/forms/settings_upsert.go +++ b/forms/settings_upsert.go @@ -5,22 +5,56 @@ import ( "time" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" ) -// SettingsUpsert defines app settings upsert form. +// SettingsUpsert specifies a [core.Settings] upsert (create/update) form. type SettingsUpsert struct { *core.Settings - app core.App + config SettingsUpsertConfig } -// NewSettingsUpsert creates new settings upsert form from the provided app. +// SettingsUpsertConfig is the [SettingsUpsert] factory initializer config. +// +// NB! App is required struct member. +type SettingsUpsertConfig struct { + App core.App + TxDao *daos.Dao + TxLogsDao *daos.Dao +} + +// NewSettingsUpsert creates a new [SettingsUpsert] form with initializer +// config created from the provided [core.App] instance. +// +// If you want to submit the form as part of another transaction, use +// [NewSettingsUpsertWithConfig] with explicitly set TxDao. func NewSettingsUpsert(app core.App) *SettingsUpsert { - form := &SettingsUpsert{app: app} + return NewSettingsUpsertWithConfig(SettingsUpsertConfig{ + App: app, + }) +} + +// NewSettingsUpsertWithConfig creates a new [SettingsUpsert] form +// with the provided config or panics on invalid configuration. +func NewSettingsUpsertWithConfig(config SettingsUpsertConfig) *SettingsUpsert { + form := &SettingsUpsert{config: config} + + if form.config.App == nil { + panic("Missing required config.App instance.") + } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + + if form.config.TxLogsDao == nil { + form.config.TxLogsDao = form.config.App.LogsDao() + } // load the application settings into the form - form.Settings, _ = app.Settings().Clone() + form.Settings, _ = config.App.Settings().Clone() return form } @@ -41,10 +75,10 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error { return err } - encryptionKey := os.Getenv(form.app.EncryptionEnv()) + encryptionKey := os.Getenv(form.config.App.EncryptionEnv()) return runInterceptors(func() error { - saveErr := form.app.Dao().SaveParam( + saveErr := form.config.TxDao.SaveParam( models.ParamAppSettings, form.Settings, encryptionKey, @@ -54,11 +88,11 @@ func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error { } // explicitly trigger old logs deletion - form.app.LogsDao().DeleteOldRequests( + form.config.TxLogsDao.DeleteOldRequests( time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays), ) // merge the application settings with the form ones - return form.app.Settings().Merge(form.Settings) + return form.config.App.Settings().Merge(form.Settings) }, interceptors...) } diff --git a/forms/user_email_change_confirm.go b/forms/user_email_change_confirm.go index cc5a44b6..a83bfa8a 100644 --- a/forms/user_email_change_confirm.go +++ b/forms/user_email_change_confirm.go @@ -3,23 +3,53 @@ package forms import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tools/security" ) -// UserEmailChangeConfirm defines a user email change confirmation form. +// UserEmailChangeConfirm specifies a user email change confirmation form. type UserEmailChangeConfirm struct { - app core.App + config UserEmailChangeConfirmConfig Token string `form:"token" json:"token"` Password string `form:"password" json:"password"` } -// NewUserEmailChangeConfirm creates new user email change confirmation form. +// UserEmailChangeConfirmConfig is the [UserEmailChangeConfirm] factory initializer config. +// +// NB! App is required struct member. +type UserEmailChangeConfirmConfig struct { + App core.App + TxDao *daos.Dao +} + +// NewUserEmailChangeConfirm creates a new [UserEmailChangeConfirm] +// form with initializer config created from the provided [core.App] instance. +// +// This factory method is used primarily for convenience (and backward compatibility). +// If you want to submit the form as part of another transaction, use +// [NewUserEmailChangeConfirmWithConfig] with explicitly set TxDao. func NewUserEmailChangeConfirm(app core.App) *UserEmailChangeConfirm { - return &UserEmailChangeConfirm{ - app: app, + return NewUserEmailChangeConfirmWithConfig(UserEmailChangeConfirmConfig{ + App: app, + }) +} + +// NewUserEmailChangeConfirmWithConfig creates a new [UserEmailChangeConfirm] +// form with the provided config or panics on invalid configuration. +func NewUserEmailChangeConfirmWithConfig(config UserEmailChangeConfirmConfig) *UserEmailChangeConfirm { + form := &UserEmailChangeConfirm{config: config} + + if form.config.App == nil { + panic("Missing required config.App instance.") } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + + return form } // Validate makes the form validatable by implementing [validation.Validatable] interface. @@ -73,14 +103,14 @@ func (form *UserEmailChangeConfirm) parseToken(token string) (*models.User, stri } // ensure that there aren't other users with the new email - if !form.app.Dao().IsUserEmailUnique(newEmail, "") { + if !form.config.TxDao.IsUserEmailUnique(newEmail, "") { return nil, "", validation.NewError("validation_existing_token_email", "The new email address is already registered: "+newEmail) } - // verify that the token is not expired and its signiture is valid - user, err := form.app.Dao().FindUserByToken( + // verify that the token is not expired and its signature is valid + user, err := form.config.TxDao.FindUserByToken( token, - form.app.Settings().UserEmailChangeToken.Secret, + form.config.App.Settings().UserEmailChangeToken.Secret, ) if err != nil || user == nil { return nil, "", validation.NewError("validation_invalid_token", "Invalid or expired token.") @@ -105,7 +135,7 @@ func (form *UserEmailChangeConfirm) Submit() (*models.User, error) { user.Verified = true user.RefreshTokenKey() // invalidate old tokens - if err := form.app.Dao().SaveUser(user); err != nil { + if err := form.config.TxDao.SaveUser(user); err != nil { return nil, err } diff --git a/forms/user_email_change_request.go b/forms/user_email_change_request.go index 8e27b045..a1a2d32f 100644 --- a/forms/user_email_change_request.go +++ b/forms/user_email_change_request.go @@ -4,24 +4,55 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/mails" "github.com/pocketbase/pocketbase/models" ) // UserEmailChangeRequest defines a user email change request form. type UserEmailChangeRequest struct { - app core.App - user *models.User + config UserEmailChangeRequestConfig + user *models.User NewEmail string `form:"newEmail" json:"newEmail"` } -// NewUserEmailChangeRequest creates a new user email change request form. +// UserEmailChangeRequestConfig is the [UserEmailChangeRequest] factory initializer config. +// +// NB! App is required struct member. +type UserEmailChangeRequestConfig struct { + App core.App + TxDao *daos.Dao +} + +// NewUserEmailChangeRequest creates a new [UserEmailChangeRequest] +// form with initializer config created from the provided [core.App] instance. +// +// If you want to submit the form as part of another transaction, use +// [NewUserEmailChangeConfirmWithConfig] with explicitly set TxDao. func NewUserEmailChangeRequest(app core.App, user *models.User) *UserEmailChangeRequest { - return &UserEmailChangeRequest{ - app: app, - user: user, + return NewUserEmailChangeRequestWithConfig(UserEmailChangeRequestConfig{ + App: app, + }, user) +} + +// NewUserEmailChangeRequestWithConfig creates a new [UserEmailChangeRequest] +// form with the provided config or panics on invalid configuration. +func NewUserEmailChangeRequestWithConfig(config UserEmailChangeRequestConfig, user *models.User) *UserEmailChangeRequest { + form := &UserEmailChangeRequest{ + config: config, + user: user, } + + if form.config.App == nil { + panic("Missing required config.App instance.") + } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + + return form } // Validate makes the form validatable by implementing [validation.Validatable] interface. @@ -40,7 +71,7 @@ func (form *UserEmailChangeRequest) Validate() error { func (form *UserEmailChangeRequest) checkUniqueEmail(value any) error { v, _ := value.(string) - if !form.app.Dao().IsUserEmailUnique(v, "") { + if !form.config.TxDao.IsUserEmailUnique(v, "") { return validation.NewError("validation_user_email_exists", "User email already exists.") } @@ -53,5 +84,5 @@ func (form *UserEmailChangeRequest) Submit() error { return err } - return mails.SendUserChangeEmail(form.app, form.user, form.NewEmail) + return mails.SendUserChangeEmail(form.config.App, form.user, form.NewEmail) } diff --git a/forms/user_email_login.go b/forms/user_email_login.go index 49e49d15..1b8d73a9 100644 --- a/forms/user_email_login.go +++ b/forms/user_email_login.go @@ -4,21 +4,49 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" ) -// UserEmailLogin defines a user email/pass login form. +// UserEmailLogin specifies a user email/pass login form. type UserEmailLogin struct { - app core.App + config UserEmailLoginConfig Email string `form:"email" json:"email"` Password string `form:"password" json:"password"` } -// NewUserEmailLogin creates a new user email/pass login form. +// UserEmailLoginConfig is the [UserEmailLogin] factory initializer config. +// +// NB! App is required struct member. +type UserEmailLoginConfig struct { + App core.App + TxDao *daos.Dao +} + +// NewUserEmailLogin creates a new [UserEmailLogin] form with +// initializer config created from the provided [core.App] instance. +// +// This factory method is used primarily for convenience (and backward compatibility). +// If you want to submit the form as part of another transaction, use +// [NewUserEmailLoginWithConfig] with explicitly set TxDao. func NewUserEmailLogin(app core.App) *UserEmailLogin { - form := &UserEmailLogin{ - app: app, + return NewUserEmailLoginWithConfig(UserEmailLoginConfig{ + App: app, + }) +} + +// NewUserEmailLoginWithConfig creates a new [UserEmailLogin] +// form with the provided config or panics on invalid configuration. +func NewUserEmailLoginWithConfig(config UserEmailLoginConfig) *UserEmailLogin { + form := &UserEmailLogin{config: config} + + if form.config.App == nil { + panic("Missing required config.App instance.") + } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() } return form @@ -39,7 +67,7 @@ func (form *UserEmailLogin) Submit() (*models.User, error) { return nil, err } - user, err := form.app.Dao().FindUserByEmail(form.Email) + user, err := form.config.TxDao.FindUserByEmail(form.Email) if err != nil { return nil, err } diff --git a/forms/user_oauth2_login.go b/forms/user_oauth2_login.go index df39d7dc..a4d35571 100644 --- a/forms/user_oauth2_login.go +++ b/forms/user_oauth2_login.go @@ -7,15 +7,16 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tools/auth" "github.com/pocketbase/pocketbase/tools/security" "golang.org/x/oauth2" ) -// UserOauth2Login defines a user Oauth2 login form. +// UserOauth2Login specifies a user Oauth2 login form. type UserOauth2Login struct { - app core.App + config UserOauth2LoginConfig // The name of the OAuth2 client provider (eg. "google") Provider string `form:"provider" json:"provider"` @@ -30,9 +31,39 @@ type UserOauth2Login struct { RedirectUrl string `form:"redirectUrl" json:"redirectUrl"` } -// NewUserOauth2Login creates a new user Oauth2 login form. +// UserOauth2LoginConfig is the [UserOauth2Login] factory initializer config. +// +// NB! App is required struct member. +type UserOauth2LoginConfig struct { + App core.App + TxDao *daos.Dao +} + +// NewUserOauth2Login creates a new [UserOauth2Login] form with +// initializer config created from the provided [core.App] instance. +// +// If you want to submit the form as part of another transaction, use +// [NewUserOauth2LoginWithConfig] with explicitly set TxDao. func NewUserOauth2Login(app core.App) *UserOauth2Login { - return &UserOauth2Login{app: app} + return NewUserOauth2LoginWithConfig(UserOauth2LoginConfig{ + App: app, + }) +} + +// NewUserOauth2LoginWithConfig creates a new [UserOauth2Login] +// form with the provided config or panics on invalid configuration. +func NewUserOauth2LoginWithConfig(config UserOauth2LoginConfig) *UserOauth2Login { + form := &UserOauth2Login{config: config} + + if form.config.App == nil { + panic("Missing required config.App instance.") + } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + + return form } // Validate makes the form validatable by implementing [validation.Validatable] interface. @@ -48,7 +79,7 @@ func (form *UserOauth2Login) Validate() error { func (form *UserOauth2Login) checkProviderName(value any) error { name, _ := value.(string) - config, ok := form.app.Settings().NamedAuthProviderConfigs()[name] + config, ok := form.config.App.Settings().NamedAuthProviderConfigs()[name] if !ok || !config.Enabled { return validation.NewError("validation_invalid_provider", fmt.Sprintf("%q is missing or is not enabled.", name)) } @@ -68,7 +99,7 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) { return nil, nil, err } - config := form.app.Settings().NamedAuthProviderConfigs()[form.Provider] + config := form.config.App.Settings().NamedAuthProviderConfigs()[form.Provider] config.SetupProvider(provider) provider.SetRedirectUrl(form.RedirectUrl) @@ -89,12 +120,12 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) { } // login/register the auth user - user, _ := form.app.Dao().FindUserByEmail(authData.Email) + user, _ := form.config.TxDao.FindUserByEmail(authData.Email) if user != nil { // update the existing user's verified state if !user.Verified { user.Verified = true - if err := form.app.Dao().SaveUser(user); err != nil { + if err := form.config.TxDao.SaveUser(user); err != nil { return nil, authData, err } } @@ -108,7 +139,10 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) { // create new user user = &models.User{Verified: true} - upsertForm := NewUserUpsert(form.app, user) + upsertForm := NewUserUpsertWithConfig(UserUpsertConfig{ + App: form.config.App, + TxDao: form.config.TxDao, + }, user) upsertForm.Email = authData.Email upsertForm.Password = security.RandomString(30) upsertForm.PasswordConfirm = upsertForm.Password @@ -118,7 +152,7 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) { AuthData: authData, } - if err := form.app.OnUserBeforeOauth2Register().Trigger(event); err != nil { + if err := form.config.App.OnUserBeforeOauth2Register().Trigger(event); err != nil { return nil, authData, err } @@ -126,7 +160,7 @@ func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) { return nil, authData, err } - if err := form.app.OnUserAfterOauth2Register().Trigger(event); err != nil { + if err := form.config.App.OnUserAfterOauth2Register().Trigger(event); err != nil { return nil, authData, err } diff --git a/forms/user_password_reset_confirm.go b/forms/user_password_reset_confirm.go index e97ce29e..c2f88092 100644 --- a/forms/user_password_reset_confirm.go +++ b/forms/user_password_reset_confirm.go @@ -3,29 +3,59 @@ package forms import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/forms/validators" "github.com/pocketbase/pocketbase/models" ) -// UserPasswordResetConfirm defines a user password reset confirmation form. +// UserPasswordResetConfirm specifies a user password reset confirmation form. type UserPasswordResetConfirm struct { - app core.App + config UserPasswordResetConfirmConfig Token string `form:"token" json:"token"` Password string `form:"password" json:"password"` PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` } -// NewUserPasswordResetConfirm creates new user password reset confirmation form. +// UserPasswordResetConfirmConfig is the [UserPasswordResetConfirm] +// factory initializer config. +// +// NB! App is required struct member. +type UserPasswordResetConfirmConfig struct { + App core.App + TxDao *daos.Dao +} + +// NewUserPasswordResetConfirm creates a new [UserPasswordResetConfirm] +// form with initializer config created from the provided [core.App] instance. +// +// If you want to submit the form as part of another transaction, use +// [NewUserPasswordResetConfirmWithConfig] with explicitly set TxDao. func NewUserPasswordResetConfirm(app core.App) *UserPasswordResetConfirm { - return &UserPasswordResetConfirm{ - app: app, + return NewUserPasswordResetConfirmWithConfig(UserPasswordResetConfirmConfig{ + App: app, + }) +} + +// NewUserPasswordResetConfirmWithConfig creates a new [UserPasswordResetConfirm] +// form with the provided config or panics on invalid configuration. +func NewUserPasswordResetConfirmWithConfig(config UserPasswordResetConfirmConfig) *UserPasswordResetConfirm { + form := &UserPasswordResetConfirm{config: config} + + if form.config.App == nil { + panic("Missing required config.App instance.") } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + + return form } // Validate makes the form validatable by implementing [validation.Validatable] interface. func (form *UserPasswordResetConfirm) Validate() error { - minPasswordLength := form.app.Settings().EmailAuth.MinPasswordLength + minPasswordLength := form.config.App.Settings().EmailAuth.MinPasswordLength return validation.ValidateStruct(form, validation.Field(&form.Token, validation.Required, validation.By(form.checkToken)), @@ -40,9 +70,9 @@ func (form *UserPasswordResetConfirm) checkToken(value any) error { return nil // nothing to check } - user, err := form.app.Dao().FindUserByToken( + user, err := form.config.TxDao.FindUserByToken( v, - form.app.Settings().UserPasswordResetToken.Secret, + form.config.App.Settings().UserPasswordResetToken.Secret, ) if err != nil || user == nil { return validation.NewError("validation_invalid_token", "Invalid or expired token.") @@ -58,9 +88,9 @@ func (form *UserPasswordResetConfirm) Submit() (*models.User, error) { return nil, err } - user, err := form.app.Dao().FindUserByToken( + user, err := form.config.TxDao.FindUserByToken( form.Token, - form.app.Settings().UserPasswordResetToken.Secret, + form.config.App.Settings().UserPasswordResetToken.Secret, ) if err != nil { return nil, err @@ -70,7 +100,7 @@ func (form *UserPasswordResetConfirm) Submit() (*models.User, error) { return nil, err } - if err := form.app.Dao().SaveUser(user); err != nil { + if err := form.config.TxDao.SaveUser(user); err != nil { return nil, err } diff --git a/forms/user_password_reset_request.go b/forms/user_password_reset_request.go index f879e1a6..9d7f939b 100644 --- a/forms/user_password_reset_request.go +++ b/forms/user_password_reset_request.go @@ -7,24 +7,54 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/mails" "github.com/pocketbase/pocketbase/tools/types" ) -// UserPasswordResetRequest defines a user password reset request form. +// UserPasswordResetRequest specifies a user password reset request form. type UserPasswordResetRequest struct { - app core.App - resendThreshold float64 + config UserPasswordResetRequestConfig Email string `form:"email" json:"email"` } -// NewUserPasswordResetRequest creates new user password reset request form. +// UserPasswordResetRequestConfig is the [UserPasswordResetRequest] +// factory initializer config. +// +// NB! App is required struct member. +type UserPasswordResetRequestConfig struct { + App core.App + TxDao *daos.Dao + ResendThreshold float64 // in seconds +} + +// NewUserPasswordResetRequest creates a new [UserPasswordResetRequest] +// form with initializer config created from the provided [core.App] instance. +// +// If you want to submit the form as part of another transaction, use +// [NewUserPasswordResetRequestWithConfig] with explicitly set TxDao. func NewUserPasswordResetRequest(app core.App) *UserPasswordResetRequest { - return &UserPasswordResetRequest{ - app: app, - resendThreshold: 120, // 2 min + return NewUserPasswordResetRequestWithConfig(UserPasswordResetRequestConfig{ + App: app, + ResendThreshold: 120, // 2 min + }) +} + +// NewUserPasswordResetRequestWithConfig creates a new [UserPasswordResetRequest] +// form with the provided config or panics on invalid configuration. +func NewUserPasswordResetRequestWithConfig(config UserPasswordResetRequestConfig) *UserPasswordResetRequest { + form := &UserPasswordResetRequest{config: config} + + if form.config.App == nil { + panic("Missing required config.App instance.") } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + + return form } // Validate makes the form validatable by implementing [validation.Validatable] interface. @@ -48,23 +78,23 @@ func (form *UserPasswordResetRequest) Submit() error { return err } - user, err := form.app.Dao().FindUserByEmail(form.Email) + user, err := form.config.TxDao.FindUserByEmail(form.Email) if err != nil { return err } now := time.Now().UTC() lastResetSentAt := user.LastResetSentAt.Time() - if now.Sub(lastResetSentAt).Seconds() < form.resendThreshold { + if now.Sub(lastResetSentAt).Seconds() < form.config.ResendThreshold { return errors.New("You've already requested a password reset.") } - if err := mails.SendUserPasswordReset(form.app, user); err != nil { + if err := mails.SendUserPasswordReset(form.config.App, user); err != nil { return err } // update last sent timestamp user.LastResetSentAt = types.NowDateTime() - return form.app.Dao().SaveUser(user) + return form.config.TxDao.SaveUser(user) } diff --git a/forms/user_upsert.go b/forms/user_upsert.go index b7a3c6a1..4aed3935 100644 --- a/forms/user_upsert.go +++ b/forms/user_upsert.go @@ -26,28 +26,26 @@ type UserUpsert struct { // UserUpsertConfig is the [UserUpsert] factory initializer config. // -// NB! Dao and Settings are required struct members. +// NB! App is required struct member. type UserUpsertConfig struct { - Dao *daos.Dao - Settings *core.Settings + App core.App + TxDao *daos.Dao } // NewUserUpsert creates a new [UserUpsert] form with initializer -// config created from the provided [core.App] and [models.User] instances +// config created from the provided [core.App] instance // (for create you could pass a pointer to an empty User - `&models.User{}`). // -// This factory method is used primarily for convenience (and backward compatibility). // If you want to submit the form as part of another transaction, use -// [NewUserUpsertWithConfig] with Dao configured to your txDao. +// [NewUserEmailChangeConfirmWithConfig] with explicitly set TxDao. func NewUserUpsert(app core.App, user *models.User) *UserUpsert { return NewUserUpsertWithConfig(UserUpsertConfig{ - Dao: app.Dao(), - Settings: app.Settings(), + App: app, }, user) } -// NewUserUpsertWithConfig creates a new [UserUpsert] form -// with the provided config and [models.User] instance or panics on invalid configuration +// NewUserUpsertWithConfig creates a new [UserUpsert] form with the provided +// config and [models.User] instance or panics on invalid configuration // (for create you could pass a pointer to an empty User - `&models.User{}`). func NewUserUpsertWithConfig(config UserUpsertConfig, user *models.User) *UserUpsert { form := &UserUpsert{ @@ -55,12 +53,14 @@ func NewUserUpsertWithConfig(config UserUpsertConfig, user *models.User) *UserUp user: user, } - if form.config.Dao == nil || - form.config.Settings == nil || - form.user == nil { + if form.config.App == nil || form.user == nil { panic("Invalid initializer config or nil upsert model.") } + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + // load defaults form.Id = user.Id form.Email = user.Email @@ -89,7 +89,7 @@ func (form *UserUpsert) Validate() error { validation.Field( &form.Password, validation.When(form.user.IsNew(), validation.Required), - validation.Length(form.config.Settings.EmailAuth.MinPasswordLength, 100), + validation.Length(form.config.App.Settings().EmailAuth.MinPasswordLength, 100), ), validation.Field( &form.PasswordConfirm, @@ -102,7 +102,7 @@ func (form *UserUpsert) Validate() error { func (form *UserUpsert) checkUniqueEmail(value any) error { v, _ := value.(string) - if v == "" || form.config.Dao.IsUserEmailUnique(v, form.user.Id) { + if v == "" || form.config.TxDao.IsUserEmailUnique(v, form.user.Id) { return nil } @@ -116,8 +116,8 @@ func (form *UserUpsert) checkEmailDomain(value any) error { } domain := val[strings.LastIndex(val, "@")+1:] - only := form.config.Settings.EmailAuth.OnlyDomains - except := form.config.Settings.EmailAuth.ExceptDomains + only := form.config.App.Settings().EmailAuth.OnlyDomains + except := form.config.App.Settings().EmailAuth.ExceptDomains // only domains check if len(only) > 0 && !list.ExistInSlice(domain, only) { @@ -159,6 +159,6 @@ func (form *UserUpsert) Submit(interceptors ...InterceptorFunc) error { form.user.Email = form.Email return runInterceptors(func() error { - return form.config.Dao.SaveUser(form.user) + return form.config.TxDao.SaveUser(form.user) }, interceptors...) } diff --git a/forms/user_verification_confirm.go b/forms/user_verification_confirm.go index 4bcd0935..f3b83c64 100644 --- a/forms/user_verification_confirm.go +++ b/forms/user_verification_confirm.go @@ -3,21 +3,51 @@ package forms import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" ) -// UserVerificationConfirm defines a user email confirmation form. +// UserVerificationConfirm specifies a user email verification confirmation form. type UserVerificationConfirm struct { - app core.App + config UserVerificationConfirmConfig Token string `form:"token" json:"token"` } -// NewUserVerificationConfirm creates a new user email confirmation form. +// UserVerificationConfirmConfig is the [UserVerificationConfirm] +// factory initializer config. +// +// NB! App is required struct member. +type UserVerificationConfirmConfig struct { + App core.App + TxDao *daos.Dao +} + +// NewUserVerificationConfirm creates a new [UserVerificationConfirm] +// form with initializer config created from the provided [core.App] instance. +// +// If you want to submit the form as part of another transaction, use +// [NewUserVerificationConfirmWithConfig] with explicitly set TxDao. func NewUserVerificationConfirm(app core.App) *UserVerificationConfirm { - return &UserVerificationConfirm{ - app: app, + return NewUserVerificationConfirmWithConfig(UserVerificationConfirmConfig{ + App: app, + }) +} + +// NewUserVerificationConfirmWithConfig creates a new [UserVerificationConfirmConfig] +// form with the provided config or panics on invalid configuration. +func NewUserVerificationConfirmWithConfig(config UserVerificationConfirmConfig) *UserVerificationConfirm { + form := &UserVerificationConfirm{config: config} + + if form.config.App == nil { + panic("Missing required config.App instance.") } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + + return form } // Validate makes the form validatable by implementing [validation.Validatable] interface. @@ -33,9 +63,9 @@ func (form *UserVerificationConfirm) checkToken(value any) error { return nil // nothing to check } - user, err := form.app.Dao().FindUserByToken( + user, err := form.config.TxDao.FindUserByToken( v, - form.app.Settings().UserVerificationToken.Secret, + form.config.App.Settings().UserVerificationToken.Secret, ) if err != nil || user == nil { return validation.NewError("validation_invalid_token", "Invalid or expired token.") @@ -51,9 +81,9 @@ func (form *UserVerificationConfirm) Submit() (*models.User, error) { return nil, err } - user, err := form.app.Dao().FindUserByToken( + user, err := form.config.TxDao.FindUserByToken( form.Token, - form.app.Settings().UserVerificationToken.Secret, + form.config.App.Settings().UserVerificationToken.Secret, ) if err != nil { return nil, err @@ -65,7 +95,7 @@ func (form *UserVerificationConfirm) Submit() (*models.User, error) { user.Verified = true - if err := form.app.Dao().SaveUser(user); err != nil { + if err := form.config.TxDao.SaveUser(user); err != nil { return nil, err } diff --git a/forms/user_verification_request.go b/forms/user_verification_request.go index 54cb3938..1303773e 100644 --- a/forms/user_verification_request.go +++ b/forms/user_verification_request.go @@ -7,24 +7,54 @@ import ( validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/mails" "github.com/pocketbase/pocketbase/tools/types" ) // UserVerificationRequest defines a user email verification request form. type UserVerificationRequest struct { - app core.App - resendThreshold float64 + config UserVerificationRequestConfig Email string `form:"email" json:"email"` } -// NewUserVerificationRequest creates a new user email verification request form. +// UserVerificationRequestConfig is the [UserVerificationRequest] +// factory initializer config. +// +// NB! App is required struct member. +type UserVerificationRequestConfig struct { + App core.App + TxDao *daos.Dao + ResendThreshold float64 // in seconds +} + +// NewUserVerificationRequest creates a new [UserVerificationRequest] +// form with initializer config created from the provided [core.App] instance. +// +// If you want to submit the form as part of another transaction, use +// [NewUserVerificationRequestWithConfig] with explicitly set TxDao. func NewUserVerificationRequest(app core.App) *UserVerificationRequest { - return &UserVerificationRequest{ - app: app, - resendThreshold: 120, // 2 min + return NewUserVerificationRequestWithConfig(UserVerificationRequestConfig{ + App: app, + ResendThreshold: 120, // 2 min + }) +} + +// NewUserVerificationRequestWithConfig creates a new [UserVerificationRequest] +// form with the provided config or panics on invalid configuration. +func NewUserVerificationRequestWithConfig(config UserVerificationRequestConfig) *UserVerificationRequest { + form := &UserVerificationRequest{config: config} + + if form.config.App == nil { + panic("Missing required config.App instance.") } + + if form.config.TxDao == nil { + form.config.TxDao = form.config.App.Dao() + } + + return form } // Validate makes the form validatable by implementing [validation.Validatable] interface. @@ -48,7 +78,7 @@ func (form *UserVerificationRequest) Submit() error { return err } - user, err := form.app.Dao().FindUserByEmail(form.Email) + user, err := form.config.TxDao.FindUserByEmail(form.Email) if err != nil { return err } @@ -59,16 +89,16 @@ func (form *UserVerificationRequest) Submit() error { now := time.Now().UTC() lastVerificationSentAt := user.LastVerificationSentAt.Time() - if (now.Sub(lastVerificationSentAt)).Seconds() < form.resendThreshold { + if (now.Sub(lastVerificationSentAt)).Seconds() < form.config.ResendThreshold { return errors.New("A verification email was already sent.") } - if err := mails.SendUserVerification(form.app, user); err != nil { + if err := mails.SendUserVerification(form.config.App, user); err != nil { return err } // update last sent timestamp user.LastVerificationSentAt = types.NowDateTime() - return form.app.Dao().SaveUser(user) + return form.config.TxDao.SaveUser(user) }