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 @@
@@ -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'));
+ }
}