diff --git a/src/app/Http/Controllers/API/AuthController.php b/src/app/Http/Controllers/API/AuthController.php --- a/src/app/Http/Controllers/API/AuthController.php +++ b/src/app/Http/Controllers/API/AuthController.php @@ -82,7 +82,7 @@ $user = \App\User::where('email', $request->email)->first(); if (!$user) { - return response()->json(['status' => 'error', 'message' => __('auth.failed')], 401); + return response()->json(['status' => 'error', 'message' => \trans('auth.failed')], 401); } return self::logonResponse($user, $request->password, $request->secondfactor); @@ -107,7 +107,7 @@ $refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId); return response()->json([ 'status' => 'success', - 'message' => __('auth.logoutsuccess') + 'message' => \trans('auth.logoutsuccess') ]); } @@ -161,7 +161,7 @@ return response()->json(['status' => 'error', 'errors' => $errors], 422); } - return response()->json(['status' => 'error', 'message' => __('auth.failed')], 401); + return response()->json(['status' => 'error', 'message' => \trans('auth.failed')], 401); } $response['access_token'] = $data->access_token; diff --git a/src/app/Http/Controllers/API/PasswordResetController.php b/src/app/Http/Controllers/API/PasswordResetController.php --- a/src/app/Http/Controllers/API/PasswordResetController.php +++ b/src/app/Http/Controllers/API/PasswordResetController.php @@ -41,12 +41,12 @@ $user = User::findByEmail($request->email); if (!$user) { - $errors = ['email' => __('validation.usernotexists')]; + $errors = ['email' => \trans('validation.usernotexists')]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } if (!$user->getSetting('external_email')) { - $errors = ['email' => __('validation.noextemail')]; + $errors = ['email' => \trans('validation.noextemail')]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php --- a/src/app/Http/Controllers/API/SignupController.php +++ b/src/app/Http/Controllers/API/SignupController.php @@ -48,7 +48,7 @@ $plans[] = [ 'title' => $plan->title, 'name' => $plan->name, - 'button' => __('app.planbutton', ['plan' => $plan->name]), + 'button' => \trans('app.planbutton', ['plan' => $plan->name]), 'description' => $plan->description, ]; }); diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php --- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php @@ -74,7 +74,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.domain-suspend-success'), + 'message' => \trans('app.domain-suspend-success'), ]); } @@ -98,7 +98,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.domain-unsuspend-success'), + 'message' => \trans('app.domain-unsuspend-success'), ]); } } diff --git a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php --- a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php @@ -88,7 +88,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.distlist-suspend-success'), + 'message' => \trans('app.distlist-suspend-success'), ]); } @@ -112,7 +112,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.distlist-unsuspend-success'), + 'message' => \trans('app.distlist-unsuspend-success'), ]); } } 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 @@ -173,7 +173,57 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.user-reset-2fa-success'), + 'message' => \trans('app.user-reset-2fa-success'), + ]); + } + + /** + * Set/Add a SKU for the user + * + * @param \Illuminate\Http\Request $request The API request. + * @param string $id User identifier + * @param string $sku SKU title + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function setSku(Request $request, $id, $sku) + { + // For now we allow adding the 'beta' SKU only + if ($sku != 'beta') { + return $this->errorResponse(404); + } + + $user = User::find($id); + + if (!$this->checkTenant($user)) { + return $this->errorResponse(404); + } + + if (!$this->guard()->user()->canUpdate($user)) { + return $this->errorResponse(403); + } + + $sku = Sku::withObjectTenantContext($user)->where('title', $sku)->first(); + + if (!$sku) { + return $this->errorResponse(404); + } + + if ($user->entitlements()->where('sku_id', $sku->id)->first()) { + return $this->errorResponse(422, \trans('app.user-set-sku-already-exists')); + } + + $user->assignSku($sku); + $entitlement = $user->entitlements()->where('sku_id', $sku->id)->first(); + + return response()->json([ + 'status' => 'success', + 'message' => \trans('app.user-set-sku-success'), + 'sku' => [ + 'cost' => $entitlement->cost, + 'name' => $sku->name, + 'id' => $sku->id, + ] ]); } @@ -251,7 +301,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.user-suspend-success'), + 'message' => \trans('app.user-suspend-success'), ]); } @@ -279,7 +329,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.user-unsuspend-success'), + 'message' => \trans('app.user-unsuspend-success'), ]); } @@ -327,7 +377,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.user-update-success'), + 'message' => \trans('app.user-update-success'), ]); } } diff --git a/src/app/Http/Controllers/API/V4/GroupsController.php b/src/app/Http/Controllers/API/V4/GroupsController.php --- a/src/app/Http/Controllers/API/V4/GroupsController.php +++ b/src/app/Http/Controllers/API/V4/GroupsController.php @@ -46,7 +46,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.distlist-delete-success'), + 'message' => \trans('app.distlist-delete-success'), ]); } @@ -294,7 +294,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.distlist-create-success'), + 'message' => \trans('app.distlist-create-success'), ]); } @@ -352,7 +352,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.distlist-update-success'), + 'message' => \trans('app.distlist-update-success'), ]); } diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -62,7 +62,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.user-delete-success'), + 'message' => \trans('app.user-delete-success'), ]); } @@ -113,7 +113,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.user-setconfig-success'), + 'message' => \trans('app.user-setconfig-success'), ]); } @@ -359,7 +359,7 @@ return response()->json([ 'status' => 'success', - 'message' => __('app.user-create-success'), + 'message' => \trans('app.user-create-success'), ]); } @@ -420,7 +420,7 @@ $response = [ 'status' => 'success', - 'message' => __('app.user-update-success'), + 'message' => \trans('app.user-update-success'), ]; // For self-update refresh the statusInfo in the UI 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 @@ -54,6 +54,8 @@ 'user-unsuspend-success' => 'User unsuspended successfully.', 'user-reset-2fa-success' => '2-Factor authentication reset successfully.', 'user-setconfig-success' => 'User settings updated successfully.', + 'user-set-sku-success' => 'The subscription added successfully.', + 'user-set-sku-already-exists' => 'The subscription already exists.', 'search-foundxdomains' => ':x domains have been found.', 'search-foundxgroups' => ':x distribution lists have been found.', 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 @@ -322,6 +322,7 @@ 'user' => [ '2fa-hint1' => "This will remove 2-Factor Authentication entitlement as well as the user-configured factors.", '2fa-hint2' => "Please, make sure to confirm the user identity properly.", + 'add-beta' => "Enable beta program", 'address' => "Address", 'aliases' => "Aliases", 'aliases-email' => "Email Aliases", 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 @@ -224,6 +224,9 @@ + @@ -451,6 +454,7 @@ discounts: [], external_email: '', has2FA: false, + hasBeta: false, wallet: {}, walletReload: false, distlists: [], @@ -529,6 +533,8 @@ if (sku.handler == 'auth2f') { this.has2FA = true this.sku2FA = sku.id + } else if (sku.handler == 'beta') { + this.hasBeta = true } } }) @@ -559,6 +565,22 @@ $(this.$el).find('ul.nav-tabs a').on('click', this.$root.tab) }, methods: { + addBetaSku() { + axios.post('/api/v4/users/' + this.user.id + '/skus/beta') + .then(response => { + if (response.data.status == 'success') { + this.$toast.success(response.data.message) + this.hasBeta = true + const sku = response.data.sku + this.skus.push({ + id: sku.id, + name: sku.name, + cost: sku.cost, + price: this.$root.priceLabel(sku.cost, this.discount) + }) + } + }) + }, capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1) }, diff --git a/src/routes/api.php b/src/routes/api.php --- a/src/routes/api.php +++ b/src/routes/api.php @@ -188,6 +188,7 @@ Route::get('users/{id}/discounts', 'API\V4\Reseller\DiscountsController@userDiscounts'); Route::post('users/{id}/reset2FA', 'API\V4\Admin\UsersController@reset2FA'); Route::get('users/{id}/skus', 'API\V4\Admin\SkusController@userSkus'); + Route::post('users/{id}/skus/{sku}', 'API\V4\Admin\UsersController@setSku'); Route::post('users/{id}/suspend', 'API\V4\Admin\UsersController@suspend'); Route::post('users/{id}/unsuspend', 'API\V4\Admin\UsersController@unsuspend'); Route::apiResource('wallets', API\V4\Admin\WalletsController::class); @@ -232,6 +233,7 @@ Route::get('users/{id}/discounts', 'API\V4\Reseller\DiscountsController@userDiscounts'); Route::post('users/{id}/reset2FA', 'API\V4\Reseller\UsersController@reset2FA'); Route::get('users/{id}/skus', 'API\V4\Reseller\SkusController@userSkus'); + Route::post('users/{id}/skus/{sku}', 'API\V4\Admin\UsersController@setSku'); Route::post('users/{id}/suspend', 'API\V4\Reseller\UsersController@suspend'); Route::post('users/{id}/unsuspend', 'API\V4\Reseller\UsersController@unsuspend'); Route::apiResource('wallets', API\V4\Reseller\WalletsController::class); 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 @@ -368,7 +368,8 @@ ->assertSeeIn('table tbody tr:nth-child(6) td:last-child', '45,09 CHF/month¹') ->assertMissing('table tfoot') ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher') - ->assertSeeIn('#reset2fa', 'Reset 2-Factor Auth'); + ->assertSeeIn('#reset2fa', 'Reset 2-Factor Auth') + ->assertMissing('#addbetasku'); }); // We don't expect John's domains here @@ -515,4 +516,29 @@ ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)'); }); } + + /** + * Test adding the beta SKU for the user + */ + public function testAddBetaSku(): void + { + $this->browse(function (Browser $browser) { + $this->deleteTestUser('userstest1@kolabnow.com'); + $user = $this->getTestUser('userstest1@kolabnow.com'); + $sku = Sku::withEnvTenantContext()->where('title', 'beta')->first(); + + $browser->visit(new UserPage($user->id)) + ->click('@nav #tab-subscriptions') + ->waitFor('@user-subscriptions #addbetasku') + ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)') + ->assertSeeIn('#addbetasku', 'Enable beta program') + ->click('#addbetasku') + ->assertToast(Toast::TYPE_SUCCESS, 'The subscription added successfully.') + ->waitFor('#sku' . $sku->id) + ->assertSeeIn("#sku{$sku->id} td:first-child", 'Private Beta (invitation only)') + ->assertSeeIn("#sku{$sku->id} td:last-child", '0,00 CHF/month') + ->assertMissing('#addbetasku') + ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (1)'); + }); + } } diff --git a/src/tests/Browser/Reseller/UserTest.php b/src/tests/Browser/Reseller/UserTest.php --- a/src/tests/Browser/Reseller/UserTest.php +++ b/src/tests/Browser/Reseller/UserTest.php @@ -480,4 +480,29 @@ ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)'); }); } + + /** + * Test adding the beta SKU for the user + */ + public function testAddBetaSku(): void + { + $this->browse(function (Browser $browser) { + $this->deleteTestUser('userstest1@kolabnow.com'); + $user = $this->getTestUser('userstest1@kolabnow.com'); + $sku = Sku::withEnvTenantContext()->where('title', 'beta')->first(); + + $browser->visit(new UserPage($user->id)) + ->click('@nav #tab-subscriptions') + ->waitFor('@user-subscriptions #addbetasku') + ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)') + ->assertSeeIn('#addbetasku', 'Enable beta program') + ->click('#addbetasku') + ->assertToast(Toast::TYPE_SUCCESS, 'The subscription added successfully.') + ->waitFor('#sku' . $sku->id) + ->assertSeeIn("#sku{$sku->id} td:first-child", 'Private Beta (invitation only)') + ->assertSeeIn("#sku{$sku->id} td:last-child", '0,00 CHF/month') + ->assertMissing('#addbetasku') + ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (1)'); + }); + } } 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 @@ -282,6 +282,8 @@ */ public function testReset2FA(): void { + Queue::fake(); + $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $admin = $this->getTestUser('jeroen@jeroen.jeroen'); @@ -316,6 +318,58 @@ $this->assertCount(0, $sf->factors()); } + /** + * Test adding beta SKU (POST /api/v4/users//skus/beta) + */ + public function testAddBetaSku(): void + { + Queue::fake(); + + $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); + $admin = $this->getTestUser('jeroen@jeroen.jeroen'); + $sku = Sku::withEnvTenantContext()->where(['title' => 'beta'])->first(); + + // Test unauthorized access to admin API + $response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/skus/beta", []); + $response->assertStatus(403); + + // For now we allow only the beta sku + $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/skus/mailbox", []); + $response->assertStatus(404); + + $entitlements = $user->entitlements()->where('sku_id', $sku->id)->get(); + $this->assertCount(0, $entitlements); + + // Test adding the beta sku + $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/skus/beta", []); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame('success', $json['status']); + $this->assertSame("The subscription added successfully.", $json['message']); + $this->assertSame(0, $json['sku']['cost']); + $this->assertSame($sku->id, $json['sku']['id']); + $this->assertSame($sku->name, $json['sku']['name']); + $this->assertCount(3, $json); + + $entitlements = $user->entitlements()->where('sku_id', $sku->id)->get(); + $this->assertCount(1, $entitlements); + + // Test adding the beta sku again, expect an error + $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/skus/beta", []); + $response->assertStatus(422); + + $json = $response->json(); + + $this->assertSame('error', $json['status']); + $this->assertSame("The subscription already exists.", $json['message']); + $this->assertCount(2, $json); + + $entitlements = $user->entitlements()->where('sku_id', $sku->id)->get(); + $this->assertCount(1, $entitlements); + } + /** * Test user creation (POST /api/v4/users) */ 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 @@ -299,6 +299,70 @@ $this->assertCount(0, $sf->factors()); } + /** + * Test adding beta SKU (POST /api/v4/users//skus/beta) + */ + public function testAddBetaSku(): void + { + Queue::fake(); // disable jobs + + $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'); + $sku = Sku::withEnvTenantContext()->where(['title' => 'beta'])->first(); + + // Test unauthorized access to reseller API + $response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/skus/beta", []); + $response->assertStatus(403); + + $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/skus/beta", []); + $response->assertStatus(403); + + $response = $this->actingAs($reseller2)->post("/api/v4/users/{$user->id}/skus/beta", []); + $response->assertStatus(404); + + // Touching admins is forbidden + $response = $this->actingAs($reseller1)->post("/api/v4/users/{$admin->id}/skus/beta", []); + $response->assertStatus(403); + + // For now we allow only the beta sku + $response = $this->actingAs($reseller1)->post("/api/v4/users/{$user->id}/skus/mailbox", []); + $response->assertStatus(404); + + $entitlements = $user->entitlements()->where('sku_id', $sku->id)->get(); + $this->assertCount(0, $entitlements); + + // Test adding the beta sku + $response = $this->actingAs($reseller1)->post("/api/v4/users/{$user->id}/skus/beta", []); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame('success', $json['status']); + $this->assertSame("The subscription added successfully.", $json['message']); + $this->assertSame(0, $json['sku']['cost']); + $this->assertSame($sku->id, $json['sku']['id']); + $this->assertSame($sku->name, $json['sku']['name']); + $this->assertCount(3, $json); + + $entitlements = $user->entitlements()->where('sku_id', $sku->id)->get(); + $this->assertCount(1, $entitlements); + + // Test adding the beta sku again, expect an error + $response = $this->actingAs($reseller1)->post("/api/v4/users/{$user->id}/skus/beta", []); + $response->assertStatus(422); + + $json = $response->json(); + + $this->assertSame('error', $json['status']); + $this->assertSame("The subscription already exists.", $json['message']); + $this->assertCount(2, $json); + + $entitlements = $user->entitlements()->where('sku_id', $sku->id)->get(); + $this->assertCount(1, $entitlements); + } + /** * Test user creation (POST /api/v4/users) */