From 201e2ccb055c8d3821ca9ea5b518c3ed39ad2641 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Sat, 5 Apr 2025 13:35:02 +0300 Subject: [PATCH] [#6607] allowed manually changing the password from the create/update hooks --- CHANGELOG.md | 2 +- forms/record_upsert.go | 41 ++++++++++--- forms/record_upsert_test.go | 113 ++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a046c1..90c45ab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ - Soft-deprecated the `$http.send`'s `result.raw` field in favor of `result.body` that contains the response body as plain bytes slice to avoid the discrepancies between Go and the JSVM when casting binary data to string. (@todo update docs to use the new field) -- Minor UI fixes (_removed the superuser fields from the auth record create/update body examples, etc._). +- Other minor improvements (_removed the superuser fields from the auth record create/update body examples, allowed programmatically updating the auth record password from the create/update hooks, etc._). ## v0.26.6 diff --git a/forms/record_upsert.go b/forms/record_upsert.go index 653823bc..dd7ea40e 100644 --- a/forms/record_upsert.go +++ b/forms/record_upsert.go @@ -27,9 +27,10 @@ type RecordUpsert struct { accessLevel int // extra password fields - password string - passwordConfirm string - oldPassword string + disablePasswordValidations bool + password string + passwordConfirm string + oldPassword string } // NewRecordUpsert creates a new [RecordUpsert] form from the provided [core.App] and [core.Record] instances @@ -130,6 +131,8 @@ func (form *RecordUpsert) validateFormFields() error { return nil } + form.syncPasswordFields() + isNew := form.record.IsNew() original := form.record.Original() @@ -165,17 +168,17 @@ func (form *RecordUpsert) validateFormFields() error { validation.Key( "password", validation.When( - (isNew || form.passwordConfirm != "" || form.oldPassword != ""), + !form.disablePasswordValidations && (isNew || form.passwordConfirm != "" || form.oldPassword != ""), validation.Required, ), ), validation.Key( "passwordConfirm", validation.When( - (isNew || form.password != "" || form.oldPassword != ""), + !form.disablePasswordValidations && (isNew || form.password != "" || form.oldPassword != ""), validation.Required, ), - validation.By(validators.Equal(form.password)), + validation.When(!form.disablePasswordValidations, validation.By(validators.Equal(form.password))), ), validation.Key( "oldPassword", @@ -183,7 +186,7 @@ func (form *RecordUpsert) validateFormFields() error { // - form.HasManageAccess() is not satisfied // - changing the existing password validation.When( - !isNew && !form.HasManageAccess() && (form.password != "" || form.passwordConfirm != ""), + !form.disablePasswordValidations && !isNew && !form.HasManageAccess() && (form.password != "" || form.passwordConfirm != ""), validation.Required, validation.By(form.checkOldPassword), ), @@ -287,3 +290,27 @@ func (form *RecordUpsert) Submit() error { // run record validations and persist in db return form.app.SaveWithContext(form.ctx, form.record) } + +// syncPasswordFields syncs the form's auth password fields with their +// corresponding record field values. +// +// This could be useful in case the password fields were programmatically set +// directly by modifying the related record model. +func (form *RecordUpsert) syncPasswordFields() { + if !form.record.Collection().IsAuth() { + return // not an auth collection + } + + form.disablePasswordValidations = false + + rawPassword := form.record.GetRaw(core.FieldNamePassword) + if v, ok := rawPassword.(*core.PasswordFieldValue); ok && v != nil { + if + // programmatically set custom plain password value + (v.Plain != "" && v.Plain != form.password) || + // random generated password for new record + (v.Plain == "" && v.Hash != "" && form.record.IsNew()) { + form.disablePasswordValidations = true + } + } +} diff --git a/forms/record_upsert_test.go b/forms/record_upsert_test.go index 82633593..faf423d3 100644 --- a/forms/record_upsert_test.go +++ b/forms/record_upsert_test.go @@ -887,6 +887,119 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) { testFilesCount(t, testApp, record, 2) // the file + attrs } +func TestRecordUpsertPasswordsSync(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + users, err := testApp.FindCollectionByNameOrId("users") + if err != nil { + t.Fatal(err) + } + + t.Run("new user without password", func(t *testing.T) { + record := core.NewRecord(users) + + form := forms.NewRecordUpsert(testApp, record) + + err := form.Submit() + + tests.TestValidationErrors(t, err, []string{"password", "passwordConfirm"}) + }) + + t.Run("new user with manual password", func(t *testing.T) { + record := core.NewRecord(users) + + form := forms.NewRecordUpsert(testApp, record) + + record.SetPassword("1234567890") + + err := form.Submit() + if err != nil { + t.Fatalf("Expected no errors, got %v", err) + } + }) + + t.Run("new user with random password", func(t *testing.T) { + record := core.NewRecord(users) + + form := forms.NewRecordUpsert(testApp, record) + + record.SetRandomPassword() + + err := form.Submit() + if err != nil { + t.Fatalf("Expected no errors, got %v", err) + } + }) + + t.Run("update user with no password change", func(t *testing.T) { + record, err := testApp.FindAuthRecordByEmail(users, "test@example.com") + if err != nil { + t.Fatal(err) + } + + oldHash := record.GetString("password:hash") + + form := forms.NewRecordUpsert(testApp, record) + + err = form.Submit() + if err != nil { + t.Fatalf("Expected no errors, got %v", err) + } + + newHash := record.GetString("password:hash") + if newHash == "" || newHash != oldHash { + t.Fatal("Expected no password change") + } + }) + + t.Run("update user with manual password change", func(t *testing.T) { + record, err := testApp.FindAuthRecordByEmail(users, "test@example.com") + if err != nil { + t.Fatal(err) + } + + oldHash := record.GetString("password:hash") + + form := forms.NewRecordUpsert(testApp, record) + + record.SetPassword("1234567890") + + err = form.Submit() + if err != nil { + t.Fatalf("Expected no errors, got %v", err) + } + + newHash := record.GetString("password:hash") + if newHash == "" || newHash == oldHash { + t.Fatal("Expected password change") + } + }) + + t.Run("update user with random password change", func(t *testing.T) { + record, err := testApp.FindAuthRecordByEmail(users, "test@example.com") + if err != nil { + t.Fatal(err) + } + + oldHash := record.GetString("password:hash") + + form := forms.NewRecordUpsert(testApp, record) + + record.SetRandomPassword() + + err = form.Submit() + if err != nil { + t.Fatalf("Expected no errors, got %v", err) + } + + newHash := record.GetString("password:hash") + if newHash == "" || newHash == oldHash { + t.Fatal("Expected password change") + } + }) +} + // ------------------------------------------------------------------- func testFilesCount(t *testing.T, app core.App, record *core.Record, count int) {