diff --git a/ProcessMaker/Http/Controllers/Admin/UserController.php b/ProcessMaker/Http/Controllers/Admin/UserController.php index aa156968c0..b0de50310b 100644 --- a/ProcessMaker/Http/Controllers/Admin/UserController.php +++ b/ProcessMaker/Http/Controllers/Admin/UserController.php @@ -9,6 +9,7 @@ use ProcessMaker\Models\JsonData; use ProcessMaker\Models\Permission; use ProcessMaker\Models\User; +use ProcessMaker\Package\Auth\Models\SsoUser; use ProcessMaker\Traits\HasControllerAddons; class UserController extends Controller @@ -76,6 +77,10 @@ function ($result, $item) { return $result; } ); + $ssoUser = false; + if (class_exists(SsoUser::class)) { + $ssoUser = SsoUser::where('user_id', $user->id)->exists(); + } // Get global and valid 2FA preferences for the user $enabled2FA = config('password-policies.2fa_enabled', false); @@ -103,6 +108,7 @@ function ($result, $item) { 'is2FAEnabledForGroup', 'addons', 'addonsSettings', + 'ssoUser', )); } diff --git a/ProcessMaker/Http/Controllers/Api/UserController.php b/ProcessMaker/Http/Controllers/Api/UserController.php index 0a828675b4..71dd3b06ec 100644 --- a/ProcessMaker/Http/Controllers/Api/UserController.php +++ b/ProcessMaker/Http/Controllers/Api/UserController.php @@ -329,6 +329,27 @@ public function update(User $user, Request $request) return $response; } } + if ($fields['email'] !== $original['email']) { + if (!isset($fields['valpassword'])) { + return response([ + 'message' => __( + 'A valid authentication is required for for update the email.' + ), + 'errors' => [ + 'email' => [ + __( + 'The password is required.' + ), + ], + ], + ], 422); + } else { + $response = $this->validateBeforeChange($user, $fields['valpassword']); + if ($response) { + return $response; + } + } + } if (Auth::user()->is_administrator && $request->has('is_administrator')) { // user must be an admin to make another user an admin $user->is_administrator = $request->get('is_administrator'); @@ -381,6 +402,32 @@ private function validateCellPhoneNumber(User $user, $number) return false; } + /** + * Validate the phone number for SMS two-factor authentication. + * + * @param User $user User to validate + * @param mixed $password String to validate + */ + private function validateBeforeChange(User $user, $password) + { + if (!Hash::check($password, $user->password)) { + return response([ + 'message' => __( + 'A valid authentication is required for for update the email.' + ), + 'errors' => [ + 'email' => [ + __( + 'The authentication is incorrect.' + ), + ], + ], + ], 422); + } + + return false; + } + /** * Update a user's pinned BPMN elements on Modeler * diff --git a/ProcessMaker/Http/Controllers/ProfileController.php b/ProcessMaker/Http/Controllers/ProfileController.php index 57f292dabb..c3129a9085 100644 --- a/ProcessMaker/Http/Controllers/ProfileController.php +++ b/ProcessMaker/Http/Controllers/ProfileController.php @@ -6,6 +6,7 @@ use ProcessMaker\i18nHelper; use ProcessMaker\Models\JsonData; use ProcessMaker\Models\User; +use ProcessMaker\Package\Auth\Models\SsoUser; use ProcessMaker\Traits\HasControllerAddons; class ProfileController extends Controller @@ -50,6 +51,11 @@ function ($result, $item) { } ); + $ssoUser = false; + if (class_exists(SsoUser::class)) { + $ssoUser = SsoUser::where('user_id', $currentUser->id)->exists(); + } + // Get global and valid 2FA preferences for the user $enabled2FA = config('password-policies.2fa_enabled', false); $global2FAEnabled = config('password-policies.2fa_method', []); @@ -60,7 +66,7 @@ function ($result, $item) { return view('profile.edit', compact('currentUser', 'states', 'timezones', 'countries', 'datetimeFormats', 'availableLangs', - 'status', 'enabled2FA', 'global2FAEnabled', 'is2FAEnabledForGroup', 'addons')); + 'status', 'enabled2FA', 'global2FAEnabled', 'is2FAEnabledForGroup', 'addons', 'ssoUser')); } /** diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php index 062a374a78..cce84eb115 100644 --- a/resources/views/admin/users/edit.blade.php +++ b/resources/views/admin/users/edit.blade.php @@ -257,6 +257,7 @@ image: '', status: @json($status), global2FAEnabled: @json($global2FAEnabled), + ssoUser:@json($ssoUser), errors: { username: null, firstname: null, @@ -282,6 +283,8 @@ groups: [], userGroupsFilter: '', focusErrors: 'errors', + originalEmail: '', + emailHasChanged: false, } }, created() { @@ -342,6 +345,7 @@ if (created) { ProcessMaker.alert(this.$t('The user was successfully created'), 'success'); } + this.originalEmail = this.formData.email; }, watch: { selectedPermissions: function () { @@ -468,28 +472,12 @@ return true }, profileUpdate($event) { - this.resetErrors(); - if (@json($enabled2FA) && this.global2FAEnabled.length === 0) { - // User has not enabled two-factor authentication correctly - ProcessMaker.alert( - this.$t('The Two Step Authentication Method has not been set. Please contact your administrator.'), - 'warning' - ); - return false; + if(this.emailHasChanged && !this.ssoUser) { + $('#validateModal').modal('show'); + } else { + this.saveProfileChanges(); } - if (!this.validatePassword()) return false; - if (@json($enabled2FA) && typeof this.formData.preferences_2fa != "undefined" && - this.formData.preferences_2fa != null && this.formData.preferences_2fa.length < 1) return false; - ProcessMaker.apiClient.put('users/' + this.formData.id, this.formData) - .then(response => { - ProcessMaker.alert(this.$t('User Updated Successfully '), 'success'); - if (this.formData.id == window.ProcessMaker.user.id) { - window.ProcessMaker.events.$emit('update-profile-avatar'); - } - }) - .catch(error => { - this.errors = error.response.data.errors; - }); + }, permissionUpdate() { ProcessMaker.apiClient.put("/permissions", { @@ -564,7 +552,44 @@ .then(response => { this.groups = response.data.data }); - } + }, + showModal() { + $('#validateModal').modal('show'); + }, + closeModal() { + $('#validateModal').modal('hide'); + }, + saveProfileChanges() { + this.resetErrors(); + if (@json($enabled2FA) && this.global2FAEnabled.length === 0) { + // User has not enabled two-factor authentication correctly + ProcessMaker.alert( + this.$t('The Two Step Authentication Method has not been set. Please contact your administrator.'), + 'warning' + ); + return false; + } + if (!this.validatePassword()) return false; + if (@json($enabled2FA) && typeof this.formData.preferences_2fa != "undefined" && + this.formData.preferences_2fa != null && this.formData.preferences_2fa.length < 1) return false; + ProcessMaker.apiClient.put('users/' + this.formData.id, this.formData) + .then(response => { + ProcessMaker.alert(this.$t('User Updated Successfully '), 'success'); + if (this.formData.id == window.ProcessMaker.user.id) { + window.ProcessMaker.events.$emit('update-profile-avatar'); + this.originalEmail = this.formData.email; + this.formData.valpassword = ""; + } + }) + .catch(error => { + this.errors = error.response.data.errors; + }); + + this.closeModal(); + }, + checkEmailChange() { + this.emailHasChanged = this.formData.email !== this.originalEmail; + }, } }); @@ -627,3 +652,4 @@ } @endsection + diff --git a/resources/views/profile/edit.blade.php b/resources/views/profile/edit.blade.php index cdedf4da4a..cf2d387692 100644 --- a/resources/views/profile/edit.blade.php +++ b/resources/views/profile/edit.blade.php @@ -136,6 +136,7 @@ states: @json($states), status: @json($status), global2FAEnabled: @json($global2FAEnabled), + ssoUser:@json($ssoUser), errors: { username: null, firstname: null, @@ -146,6 +147,8 @@ }, confPassword: '', image: '', + originalEmail: '', + emailHasChanged: false, options: [ { src: @json($currentUser['avatar']), @@ -169,37 +172,19 @@ }); } }, + mounted() { + this.originalEmail = this.formData.email; + }, methods: { openAvatarModal() { modalVueInstance.$refs.updateAvatarModal.show(); }, profileUpdate() { - this.resetErrors(); - if (@json($enabled2FA) && this.global2FAEnabled.length === 0) { - let message = 'The Two Step Authentication Method has not been set. ' + - 'Please contact your administrator.'; - // User has not enabled two-factor authentication correctly - ProcessMaker.alert(this.$t($message), 'warning'); - return false; - } - if (!this.validatePassword()) return false; - if (@json($enabled2FA) && typeof this.formData.preferences_2fa != "undefined" && - this.formData.preferences_2fa != null && this.formData.preferences_2fa.length < 1) - return false; - if (this.image) { - this.formData.avatar = this.image; - } - if (this.image === false) { - this.formData.avatar = false; - } - ProcessMaker.apiClient.put('users/' + this.formData.id, this.formData) - .then((response) => { - ProcessMaker.alert(this.$t('Your profile was saved.'), 'success') - window.ProcessMaker.events.$emit('update-profile-avatar'); - }) - .catch(error => { - this.errors = error.response.data.errors; - }); + if(this.emailHasChanged && !this.ssoUser) { + $('#validateModal').modal('show'); + } else { + this.saveProfileChanges(); + } }, deleteAvatar() { let optionValues = formVueInstance.$data.options[0]; @@ -242,6 +227,47 @@ onClose() { window.location.href = '/admin/users'; }, + showModal() { + $('#validateModal').modal('show'); + }, + closeModal() { + $('#validateModal').modal('hide'); + }, + saveProfileChanges() { + this.resetErrors(); + if (@json($enabled2FA) && this.global2FAEnabled.length === 0) { + let message = 'The Two Step Authentication Method has not been set. ' + + 'Please contact your administrator.'; + // User has not enabled two-factor authentication correctly + ProcessMaker.alert(this.$t($message), 'warning'); + return false; + } + if (!this.validatePassword()) return false; + if (@json($enabled2FA) && typeof this.formData.preferences_2fa != "undefined" && + this.formData.preferences_2fa != null && this.formData.preferences_2fa.length < 1) + return false; + if (this.image) { + this.formData.avatar = this.image; + } + if (this.image === false) { + this.formData.avatar = false; + } + ProcessMaker.apiClient.put('users/' + this.formData.id, this.formData) + .then((response) => { + ProcessMaker.alert(this.$t('Your profile was saved.'), 'success') + window.ProcessMaker.events.$emit('update-profile-avatar'); + this.originalEmail = this.formData.email; + this.formData.valpassword = ""; + }) + .catch(error => { + this.errors = error.response.data.errors; + }); + + this.closeModal(); + }, + checkEmailChange() { + this.emailHasChanged = this.formData.email !== this.originalEmail; + }, }, computed: { state2FA() { @@ -379,7 +405,7 @@ //TODO: HANDLE CONNECTION UPDATE this.onCloseModal; - } + }, } }); diff --git a/resources/views/shared/users/profile.blade.php b/resources/views/shared/users/profile.blade.php index bd19134678..0cbea47dc9 100644 --- a/resources/views/shared/users/profile.blade.php +++ b/resources/views/shared/users/profile.blade.php @@ -44,7 +44,8 @@ class="mb-2" {!! Form::email('email', null, ['id' => 'email', 'rows' => 4, 'class'=> 'form-control', 'v-model' => 'formData.email', 'v-bind:class' => '{\'form-control\':true, - \'is-invalid\':errors.email}', 'required', 'aria-required' => 'true']) !!} + \'is-invalid\':errors.email}', 'required', 'aria-required' => 'true', + '@input' => 'checkEmailChange']) !!} @@ -159,3 +160,28 @@ class="mb-2" @endforeach @endif + + + diff --git a/tests/Feature/Api/UsersTest.php b/tests/Feature/Api/UsersTest.php index 3c9dbbd61d..7d863245a8 100644 --- a/tests/Feature/Api/UsersTest.php +++ b/tests/Feature/Api/UsersTest.php @@ -377,8 +377,12 @@ public function testUpdateUser() // Load the starting user data $verify = $this->apiCall('GET', $url); + // Send the same email to avoid the email validation + $updateData = $this->getUpdatedData(); + $updateData['email'] = $verify['email']; + // Post saved success - $response = $this->apiCall('PUT', $url, $this->getUpdatedData()); + $response = $this->apiCall('PUT', $url, $updateData); // Validate the header status code $response->assertStatus(204); @@ -397,12 +401,14 @@ public function testUpdateUserForceChangePasswordFlag() { $faker = Faker::create(); - $url = self::API_TEST_URL . '/' . User::factory()->create()->id; + $user = User::factory()->create(); + + $url = self::API_TEST_URL . '/' . $user->id; // Post saved success $response = $this->apiCall('PUT', $url, [ 'username' => 'updatemytestusername', - 'email' => $faker->email(), + 'email' => $user->email, 'firstname' => $faker->firstName(), 'lastname' => $faker->lastName(), 'status' => $faker->randomElement(['ACTIVE', 'INACTIVE']), @@ -723,8 +729,11 @@ public function testUpdateUserAdmin() // Load the starting user data $verify = $this->apiCall('GET', $url); + $updateData = $this->getUpdatedData(); + $updateData['email'] = $verify['email']; + // Post saved success - $response = $this->apiCall('PUT', $url, $this->getUpdatedData()); + $response = $this->apiCall('PUT', $url, $updateData); // Validate the header status code $response->assertStatus(204); @@ -754,7 +763,10 @@ public function testUpdateUserNotAdmin() // Load the starting user data $verify = $this->apiCall('GET', $url); - $response = $this->apiCall('PUT', $url, $this->getUpdatedData()); + $updateData = $this->getUpdatedData(); + $updateData['email'] = $verify['email']; + + $response = $this->apiCall('PUT', $url, $updateData); // Validate the header status code $response->assertStatus(403); @@ -765,8 +777,11 @@ public function testUpdateUserNotAdmin() $this->user->refresh(); $this->flushSession(); + $updateData = $this->getUpdatedData(); + $updateData['email'] = $verify['email']; + // Post saved success - $response = $this->apiCall('PUT', $url, $this->getUpdatedData()); + $response = $this->apiCall('PUT', $url, $updateData); // Validate the header status code $response->assertStatus(204); @@ -787,8 +802,12 @@ public function testDisableRecommendations() $this->assertEquals(1, RecommendationUser::where('user_id', $this->user->id)->count()); $url = self::API_TEST_URL . '/' . $this->user->id; + + $updateData = $this->getUpdatedData(); + $updateData['email'] = $this->user->email; + $data = [ - ...$this->getUpdatedData(), + ...$updateData, 'meta' => [ 'disableRecommendations' => true, ],