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 @@ -207,6 +207,37 @@ } /** + * Resync the user + * + * @param \Illuminate\Http\Request $request The API request. + * @param string $id User identifier + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function resync(Request $request, $id) + { + $user = User::find($id); + + if (!$this->checkTenant($user)) { + return $this->errorResponse(404); + } + + if (!$this->guard()->user()->canUpdate($user)) { + return $this->errorResponse(403); + } + + if (\Artisan::call('user:resync', ['user' => $user->id])) { + return $this->errorResponse(500); + } + + return response()->json([ + 'status' => 'success', + 'message' => self::trans('app.user-resync-success'), + ]); + } + + + /** * Set/Add a SKU for the user * * @param \Illuminate\Http\Request $request The API request. diff --git a/src/app/Utils.php b/src/app/Utils.php --- a/src/app/Utils.php +++ b/src/app/Utils.php @@ -606,12 +606,12 @@ * * @return string String representation, e.g. "0 %", "7.7 %" */ - public static function percent(int|float $amount, $locale = 'de_DE'): string + public static function percent(int|float $percent, $locale = 'de_DE'): string { $nf = new \NumberFormatter($locale, \NumberFormatter::PERCENT); $sep = $nf->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); - $result = sprintf('%.2f', $amount); + $result = sprintf('%.2f', $percent); $result = preg_replace('/\.00/', '', $result); $result = preg_replace('/(\.[0-9])0/', '\\1', $result); $result = str_replace('.', $sep, $result); diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php --- a/src/resources/lang/en/app.php +++ b/src/resources/lang/en/app.php @@ -111,6 +111,7 @@ 'user-update-success' => 'User data updated successfully.', 'user-create-success' => 'User created successfully.', 'user-delete-success' => 'User deleted successfully.', + 'user-resync-success' => 'User synchronization have been started.', 'user-suspend-success' => 'User suspended successfully.', 'user-unsuspend-success' => 'User unsuspended successfully.', 'user-reset-2fa-success' => '2-Factor authentication reset successfully.', diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php --- a/src/resources/lang/en/ui.php +++ b/src/resources/lang/en/ui.php @@ -29,6 +29,7 @@ 'refresh' => "Refresh", 'reset' => "Reset", 'resend' => "Resend", + 'resync' => "Resync", 'save' => "Save", 'search' => "Search", 'share' => "Share", 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 @@ -77,11 +77,11 @@
- - {{ $t('btn.suspend') }} + + {{ $t(user.isSuspended ? 'btn.unsuspend' : 'btn.suspend') }} - - {{ $t('btn.unsuspend') }} + + {{ $t('btn.resync') }}
@@ -567,6 +567,14 @@ } }) }, + resyncUser() { + axios.post('/api/v4/users/' + this.user.id + '/resync') + .then(response => { + if (response.data.status == 'success') { + this.$toast.success(response.data.message) + } + }) + }, submitDiscount() { this.$refs.discountDialog.hide() @@ -624,21 +632,12 @@ } }) }, - suspendUser() { - axios.post('/api/v4/users/' + this.user.id + '/suspend') - .then(response => { - if (response.data.status == 'success') { - this.$toast.success(response.data.message) - this.user = Object.assign({}, this.user, { isSuspended: true }) - } - }) - }, - unsuspendUser() { - axios.post('/api/v4/users/' + this.user.id + '/unsuspend') + setSuspendState() { + axios.post('/api/v4/users/' + this.user.id + '/' + (this.user.isSuspended ? 'unsuspend' : 'suspend')) .then(response => { if (response.data.status == 'success') { this.$toast.success(response.data.message) - this.user = Object.assign({}, this.user, { isSuspended: false }) + this.user = Object.assign({}, this.user, { isSuspended: !this.user.isSuspended }) } }) } diff --git a/src/routes/api.php b/src/routes/api.php --- a/src/routes/api.php +++ b/src/routes/api.php @@ -229,6 +229,7 @@ Route::get('users/{id}/discounts', [API\V4\Reseller\DiscountsController::class, 'userDiscounts']); Route::post('users/{id}/reset2FA', [API\V4\Admin\UsersController::class, 'reset2FA']); Route::post('users/{id}/resetGeoLock', [API\V4\Admin\UsersController::class, 'resetGeoLock']); + Route::post('users/{id}/resync', [API\V4\Admin\UsersController::class, 'resync']); Route::get('users/{id}/skus', [API\V4\Admin\UsersController::class, 'skus']); Route::post('users/{id}/skus/{sku}', [API\V4\Admin\UsersController::class, 'setSku']); Route::post('users/{id}/suspend', [API\V4\Admin\UsersController::class, 'suspend']); @@ -280,6 +281,7 @@ Route::get('users/{id}/discounts', [API\V4\Reseller\DiscountsController::class, 'userDiscounts']); Route::post('users/{id}/reset2FA', [API\V4\Reseller\UsersController::class, 'reset2FA']); Route::post('users/{id}/resetGeoLock', [API\V4\Reseller\UsersController::class, 'resetGeoLock']); + Route::post('users/{id}/resync', [API\V4\Reseller\UsersController::class, 'resync']); Route::get('users/{id}/skus', [API\V4\Reseller\UsersController::class, 'skus']); Route::post('users/{id}/skus/{sku}', [API\V4\Admin\UsersController::class, 'setSku']); Route::post('users/{id}/suspend', [API\V4\Reseller\UsersController::class, 'suspend']); 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 @@ -555,6 +555,21 @@ } /** + * Test the Resync button + */ + public function testResync(): void + { + $this->browse(function (Browser $browser) { + $john = $this->getTestUser('john@kolab.org'); + + $browser->visit(new UserPage($john->id)) + ->assertSeeIn('@user-info #button-resync', 'Resync') + ->click('@user-info #button-resync') + ->assertToast(Toast::TYPE_SUCCESS, "User synchronization have been started."); + }); + } + + /** * Test resetting 2FA for the user */ public function testReset2FA(): void 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 @@ -388,6 +388,33 @@ } /** + * Test resync (POST /api/v4/users//resync) + */ + public function testResync(): void + { + Queue::fake(); + + $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); + $admin = $this->getTestUser('jeroen@jeroen.jeroen'); + + // Test unauthorized access to admin API + $response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/resync", []); + $response->assertStatus(403); + + // Test resync + \Artisan::shouldReceive('call')->once()->with('user:resync', ['user' => $user->id]); + + $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/resync", []); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertCount(2, $json); + $this->assertSame('success', $json['status']); + $this->assertSame("User synchronization have been started.", $json['message']); + } + + /** * Test adding beta SKU (POST /api/v4/users//skus/beta) */ public function testAddBetaSku(): void diff --git a/src/tests/Feature/Controller/Reseller/UsersTest.php b/src/tests/Feature/Controller/Reseller/UsersTest.php --- a/src/tests/Feature/Controller/Reseller/UsersTest.php +++ b/src/tests/Feature/Controller/Reseller/UsersTest.php @@ -366,6 +366,46 @@ } /** + * Test resync (POST /api/v4/users//resync) + */ + public function testResync(): void + { + Queue::fake(); + + $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); + $admin = $this->getTestUser('jeroen@jeroen.jeroen'); + $reseller1 = $this->getTestUser('reseller@' . \config('app.domain')); + $reseller2 = $this->getTestUser('reseller@sample-tenant.dev-local'); + + // Test unauthorized access to admin API + // Test unauthorized access + $response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/resync", []); + $response->assertStatus(403); + + $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/resync", []); + $response->assertStatus(403); + + $response = $this->actingAs($reseller2)->post("/api/v4/users/{$user->id}/resync", []); + $response->assertStatus(404); + + // Touching admins is forbidden + $response = $this->actingAs($reseller1)->post("/api/v4/users/{$admin->id}/resync", []); + $response->assertStatus(403); + + // Test resync + \Artisan::shouldReceive('call')->once()->with('user:resync', ['user' => $user->id]); + + $response = $this->actingAs($reseller1)->post("/api/v4/users/{$user->id}/resync", []); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertCount(2, $json); + $this->assertSame('success', $json['status']); + $this->assertSame("User synchronization have been started.", $json['message']); + } + + /** * Test adding beta SKU (POST /api/v4/users//skus/beta) */ public function testAddBetaSku(): void