Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117754940
D1189.1775201447.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
15 KB
Referenced Files
None
Subscribers
None
D1189.1775201447.diff
View Options
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
@@ -222,10 +222,6 @@
return false;
}
- if ($this->role == "admin") {
- return true;
- }
-
$wallet = $object->wallet();
// TODO: For now controller can delete/update the account owner,
@@ -273,10 +269,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
@@ -52,7 +52,7 @@
<div class="col-sm-8">
<span class="form-control-plaintext" id="external_email">
<a v-if="user.external_email" :href="'mailto:' + user.external_email">{{ user.external_email }}</a>
- <button type="button" class="btn btn-secondary btn-sm">Edit</button>
+ <button type="button" class="btn btn-secondary btn-sm" @click="emailEdit">Edit</button>
</span>
</div>
</div>
@@ -250,6 +250,30 @@
</div>
</div>
</div>
+
+ <div id="email-dialog" class="modal" tabindex="-1" role="dialog">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">External email</h5>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <p class="form-group">
+ <input v-model="external_email" name="external_email" class="form-control">
+ </p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary modal-cancel" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-primary modal-action" @click="submitEmail()">
+ <svg-icon icon="check"></svg-icon> Submit
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</template>
@@ -268,6 +292,7 @@
discount: 0,
discount_description: '',
discounts: [],
+ external_email: '',
wallet_discount: 0,
wallet_discount_description: '',
wallet_discount_id: '',
@@ -363,7 +388,7 @@
methods: {
discountEdit() {
$('#discount-dialog')
- .on('shown.bs.modal', (e, a) => {
+ .on('shown.bs.modal', e => {
$(e.target).find('select').focus()
})
.modal()
@@ -376,8 +401,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 => {
@@ -399,6 +434,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
+ }
+ })
+ }
}
}
</script>
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();
@@ -341,6 +343,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
*
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/<user-id>)
+ */
+ 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'));
+ }
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 7:30 AM (9 h, 25 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18823025
Default Alt Text
D1189.1775201447.diff (15 KB)
Attached To
Mode
D1189: Support: External email editing
Attached
Detach File
Event Timeline