diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php --- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php +++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php @@ -5,6 +5,8 @@ use App\Domain; use App\User; use App\UserSetting; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Validator; class UsersController extends \App\Http\Controllers\API\V4\UsersController { @@ -65,4 +67,48 @@ return response()->json($result); } + + /** + * Update user data. + * + * @param \Illuminate\Http\Request $request The API request. + * @params string $id User identifier + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function update(Request $request, $id) + { + $user = User::find($id); + + if (empty($user)) { + return $this->errorResponse(404); + } + + // For now admins can change only user external email address + + $rules = []; + + if (array_key_exists('external_email', $request->input())) { + $rules['external_email'] = 'email'; + } + + // Validate input + $v = Validator::make($request->all(), $rules); + + if ($v->fails()) { + return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); + } + + // Update user settings + $settings = $request->only(array_keys($rules)); + + if (!empty($settings)) { + $user->setSettings($settings); + } + + return response()->json([ + 'status' => 'success', + 'message' => __('app.user-update-success'), + ]); + } } diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -208,10 +208,6 @@ return false; } - if ($this->role == "admin") { - return true; - } - $wallet = $object->wallet(); // TODO: For now controller can delete/update the account owner, @@ -259,10 +255,6 @@ return false; } - if ($this->role == "admin") { - return true; - } - if ($object instanceof User && $this->id == $object->id) { return true; } diff --git a/src/resources/js/app.js b/src/resources/js/app.js --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -206,10 +206,18 @@ if (error.response && status == 422) { error_msg = "Form validation error" - $.each(error.response.data.errors || {}, (idx, msg) => { - $('form').each((i, form) => { - const input_name = ($(form).data('validation-prefix') || '') + idx - const input = $('#' + input_name) + const modal = $('div.modal.show') + + $(modal.length ? modal : 'form').each((i, form) => { + form = $(form) + + $.each(error.response.data.errors || {}, (idx, msg) => { + const input_name = (form.data('validation-prefix') || '') + idx + let input = form.find('#' + input_name) + + if (!input.length) { + input = form.find('[name="' + input_name + '"]'); + } if (input.length) { // Create an error message\ @@ -243,13 +251,11 @@ input.parent().find('.invalid-feedback').remove() input.parent().append(feedback) } - - return false } - }); - }) + }) - $('form .is-invalid:not(.listinput-widget)').first().focus() + form.find('.is-invalid:not(.listinput-widget)').first().focus() + }) } else if (error.response && error.response.data) { error_msg = error.response.data.message diff --git a/src/resources/sass/toast.scss b/src/resources/sass/toast.scss --- a/src/resources/sass/toast.scss +++ b/src/resources/sass/toast.scss @@ -4,7 +4,7 @@ right: 0; margin: 0.5rem; width: 320px; - z-index: 10; + z-index: 1055; // above Bootstrap's modal backdrop and dialogs } .toast { diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue --- a/src/resources/vue/Admin/User.vue +++ b/src/resources/vue/Admin/User.vue @@ -58,7 +58,7 @@
{{ user.external_email }} - +
@@ -256,6 +256,30 @@ + + @@ -274,6 +298,7 @@ discount: 0, discount_description: '', discounts: [], + external_email: '', wallet_discount: 0, wallet_discount_description: '', wallet_discount_id: '', @@ -369,7 +394,7 @@ methods: { discountEdit() { $('#discount-dialog') - .on('shown.bs.modal', (e, a) => { + .on('shown.bs.modal', e => { $(e.target).find('select').focus() }) .modal() @@ -382,8 +407,18 @@ }) } }, + emailEdit() { + this.external_email = this.user.external_email + this.$root.clearFormValidation($('#email-dialog')) + + $('#email-dialog') + .on('shown.bs.modal', e => { + $(e.target).find('input').focus() + }) + .modal() + }, submitDiscount() { - let dialog = $('#discount-dialog').modal('hide') + $('#discount-dialog').modal('hide') axios.put('/api/v4/wallets/' + this.user.wallets[0].id, { discount: this.wallet_discount_id }) .then(response => { @@ -405,6 +440,17 @@ } }) }, + submitEmail() { + axios.put('/api/v4/users/' + this.user.id, { external_email: this.external_email }) + .then(response => { + if (response.data.status == 'success') { + $('#email-dialog').modal('hide') + this.$toast.success(response.data.message) + this.user.external_email = this.external_email + this.external_email = null // required because of Vue + } + }) + } } } diff --git a/src/resources/vue/Login.vue b/src/resources/vue/Login.vue --- a/src/resources/vue/Login.vue +++ b/src/resources/vue/Login.vue @@ -55,13 +55,11 @@ return { email: '', password: '', - secondFactor: '', - loginError: false + secondFactor: '' } }, methods: { submitLogin() { - this.loginError = false this.$root.clearFormValidation($('form.form-signin')) axios.post('/api/auth/login', { @@ -71,9 +69,7 @@ }).then(response => { // login user and redirect to dashboard this.$root.loginUser(response.data.access_token) - }).catch(error => { - this.loginError = true - }); + }) } } } diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php --- a/src/tests/Browser/Admin/UserTest.php +++ b/src/tests/Browser/Admin/UserTest.php @@ -25,6 +25,7 @@ $john = $this->getTestUser('john@kolab.org'); $john->setSettings([ 'phone' => '+48123123123', + 'external_email' => 'john.doe.external@gmail.com', ]); $wallet = $john->wallets()->first(); @@ -41,6 +42,7 @@ $john = $this->getTestUser('john@kolab.org'); $john->setSettings([ 'phone' => null, + 'external_email' => 'john.doe.external@gmail.com', ]); $wallet = $john->wallets()->first(); @@ -344,6 +346,61 @@ } /** + * Test editing an external email + * + * @depends testUserInfo2 + */ + public function testExternalEmail(): void + { + $this->browse(function (Browser $browser) { + $john = $this->getTestUser('john@kolab.org'); + + $browser->visit(new UserPage($john->id)) + ->waitFor('@user-info #external_email button') + ->click('@user-info #external_email button') + // Test dialog content, and closing it with Cancel button + ->with(new Dialog('#email-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'External email') + ->assertFocused('@body input') + ->assertValue('@body input', 'john.doe.external@gmail.com') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-cancel'); + }) + ->assertMissing('#email-dialog') + ->click('@user-info #external_email button') + // Test email validation error handling, and email update + ->with(new Dialog('#email-dialog'), function (Browser $browser) { + $browser->type('@body input', 'test') + ->click('@button-action') + ->waitFor('@body input.is-invalid') + ->assertSeeIn( + '@body input + .invalid-feedback', + 'The external email must be a valid email address.' + ) + ->assertToast(Toast::TYPE_ERROR, 'Form validation error') + ->type('@body input', 'test@test.com') + ->click('@button-action'); + }) + ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.') + ->assertSeeIn('@user-info #external_email a', 'test@test.com') + ->click('@user-info #external_email button') + ->with(new Dialog('#email-dialog'), function (Browser $browser) { + $browser->assertValue('@body input', 'test@test.com') + ->assertMissing('@body input.is-invalid') + ->assertMissing('@body input + .invalid-feedback') + ->click('@button-cancel'); + }) + ->assertSeeIn('@user-info #external_email a', 'test@test.com'); + + // $john->getSetting() may not work here as it uses internal cache + // read the value form database + $current_ext_email = $john->settings()->where('key', 'external_email')->first()->value; + $this->assertSame('test@test.com', $current_ext_email); + }); + } + + /** * Test editing wallet discount * * @depends testUserInfo2 diff --git a/src/tests/Feature/Controller/Admin/UsersTest.php b/src/tests/Feature/Controller/Admin/UsersTest.php --- a/src/tests/Feature/Controller/Admin/UsersTest.php +++ b/src/tests/Feature/Controller/Admin/UsersTest.php @@ -140,4 +140,50 @@ $this->assertSame(0, $json['count']); $this->assertCount(0, $json['list']); } + + /** + * Test user update (PUT /api/v4/users/) + */ + public function testUpdate(): void + { + $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); + $admin = $this->getTestUser('jeroen@jeroen.jeroen'); + + // Test unauthorized access to admin API + $response = $this->actingAs($user)->get("/api/v4/users/{$user->id}", []); + $response->assertStatus(403); + + // Test updatig the user data (empty data) + $response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", []); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame('success', $json['status']); + $this->assertSame("User data updated successfully.", $json['message']); + $this->assertCount(2, $json); + + // Test error handling + $post = ['external_email' => 'aaa']; + $response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", $post); + $response->assertStatus(422); + + $json = $response->json(); + + $this->assertSame('error', $json['status']); + $this->assertSame("The external email must be a valid email address.", $json['errors']['external_email'][0]); + $this->assertCount(2, $json); + + // Test real update + $post = ['external_email' => 'modified@test.com']; + $response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", $post); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame('success', $json['status']); + $this->assertSame("User data updated successfully.", $json['message']); + $this->assertCount(2, $json); + $this->assertSame('modified@test.com', $user->getSetting('external_email')); + } }