Page MenuHomePhorge

D1492.1775247363.diff
No OneTemporary

Authored By
Unknown
Size
12 KB
Referenced Files
None
Subscribers
None

D1492.1775247363.diff

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
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\API\V4\Admin;
use App\Domain;
+use App\Sku;
use App\User;
use App\UserAlias;
use App\UserSetting;
@@ -75,6 +76,36 @@
}
/**
+ * Reset 2-Factor Authentication for the user
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ * @params string $id User identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function reset2FA(Request $request, $id)
+ {
+ $user = User::find($id);
+
+ if (empty($user)) {
+ return $this->errorResponse(404);
+ }
+
+ $sku = Sku::where('title', '2fa')->first();
+
+ // Note: we do select first, so the observer can delete
+ // 2FA preferences from Roundcube database, so don't
+ // be tempted to replace first() with delete() below
+ $entitlement = $user->entitlements()->where('sku_id', $sku->id)->first();
+ $entitlement->delete();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => __('app.user-reset-2fa-success'),
+ ]);
+ }
+
+ /**
* Suspend the user
*
* @param \Illuminate\Http\Request $request The API request.
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
@@ -37,6 +37,7 @@
'user-delete-success' => 'User deleted successfully.',
'user-suspend-success' => 'User suspended successfully.',
'user-unsuspend-success' => 'User unsuspended successfully.',
+ 'user-reset-2fa-success' => '2-Factor authentication reset successfully.',
'search-foundxdomains' => ':x domains have been found.',
'search-foundxusers' => ':x user accounts have been found.',
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
@@ -176,7 +176,7 @@
<div class="tab-pane" id="user-subscriptions" role="tabpanel" aria-labelledby="tab-subscriptions">
<div class="card-body">
<div class="card-text">
- <table class="table table-sm table-hover">
+ <table class="table table-sm table-hover mb-0">
<thead class="thead-light">
<tr>
<th scope="col">Subscription</th>
@@ -184,7 +184,7 @@
</tr>
</thead>
<tbody>
- <tr v-for="(sku, sku_id) in skus" :id="'sku' + sku_id" :key="sku_id">
+ <tr v-for="(sku, sku_id) in skus" :id="'sku' + sku.id" :key="sku_id">
<td>{{ sku.name }}</td>
<td>{{ sku.price }}</td>
</tr>
@@ -199,6 +199,9 @@
<hr class="m-0">
&sup1; applied discount: {{ discount }}% - {{ discount_description }}
</small>
+ <div class="mt-2">
+ <button type="button" class="btn btn-danger" id="reset2fa" v-if="has2FA" @click="reset2FADialog">Reset 2-Factor Auth</button>
+ </div>
</div>
</div>
</div>
@@ -342,6 +345,28 @@
</div>
</div>
</div>
+
+ <div id="reset-2fa-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">2-Factor Authentication Reset</h5>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <p>This will remove 2-Factor Authentication entitlement as well
+ as the user-configured factors.</p>
+ <p>Please, make sure to confirm the user identity properly.</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-danger modal-action" @click="reset2FA()">Reset</button>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</template>
@@ -369,10 +394,12 @@
discount_description: '',
discounts: [],
external_email: '',
+ has2FA: false,
wallet: {},
walletReload: false,
domains: [],
skus: [],
+ sku2FA: null,
users: [],
user: {
aliases: [],
@@ -437,6 +464,11 @@
}
this.skus.push(item)
+
+ if (sku.title == '2fa') {
+ this.has2FA = true
+ this.sku2FA = sku.id
+ }
}
})
})
@@ -513,6 +545,20 @@
this.walletReload = true
this.$nextTick(() => { this.walletReload = false })
},
+ reset2FA() {
+ $('#reset-2fa-dialog').modal('hide')
+ axios.post('/api/v4/users/' + this.user.id + '/reset2FA')
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$toast.success(response.data.message)
+ this.skus = this.skus.filter(sku => sku.id != this.sku2FA)
+ this.has2FA = false
+ }
+ })
+ },
+ reset2FADialog() {
+ $('#reset-2fa-dialog').modal()
+ },
submitDiscount() {
$('#discount-dialog').modal('hide')
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -103,6 +103,7 @@
Route::apiResource('packages', API\V4\Admin\PackagesController::class);
Route::apiResource('skus', API\V4\Admin\SkusController::class);
Route::apiResource('users', API\V4\Admin\UsersController::class);
+ Route::post('users/{id}/reset2FA', 'API\V4\Admin\UsersController@reset2FA');
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);
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
@@ -2,7 +2,9 @@
namespace Tests\Browser\Admin;
+use App\Auth\SecondFactor;
use App\Discount;
+use App\Sku;
use App\User;
use Tests\Browser;
use Tests\Browser\Components\Dialog;
@@ -130,7 +132,8 @@
->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF')
->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '5,55 CHF')
- ->assertMissing('table tfoot');
+ ->assertMissing('table tfoot')
+ ->assertMissing('#reset2fa');
});
// Assert Domains tab
@@ -300,7 +303,8 @@
->assertSeeIn('table tbody tr:nth-child(5) td:first-child', '2-Factor Authentication')
->assertSeeIn('table tbody tr:nth-child(5) td:last-child', '0,00 CHF/month¹')
->assertMissing('table tfoot')
- ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher');
+ ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher')
+ ->assertSeeIn('#reset2fa', 'Reset 2-Factor Auth');
});
// We don't expect John's domains here
@@ -398,4 +402,36 @@
->assertMissing('@user-info #button-unsuspend');
});
}
+
+ /**
+ * Test resetting 2FA for the user
+ */
+ public function testReset2FA(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $this->deleteTestUser('userstest1@kolabnow.com');
+ $user = $this->getTestUser('userstest1@kolabnow.com');
+ $sku2fa = Sku::firstOrCreate(['title' => '2fa']);
+ $user->assignSku($sku2fa);
+ SecondFactor::seed('userstest1@kolabnow.com');
+
+ $browser->visit(new UserPage($user->id))
+ ->click('@nav #tab-subscriptions')
+ ->with('@user-subscriptions', function (Browser $browser) use ($sku2fa) {
+ $browser->waitFor('#reset2fa')
+ ->assertVisible('#sku' . $sku2fa->id);
+ })
+ ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (1)')
+ ->click('#reset2fa')
+ ->with(new Dialog('#reset-2fa-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', '2-Factor Authentication Reset')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Reset')
+ ->click('@button-action');
+ })
+ ->assertToast(Toast::TYPE_SUCCESS, '2-Factor authentication reset successfully.')
+ ->assertMissing('#sku' . $sku2fa->id)
+ ->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (0)');
+ });
+ }
}
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
@@ -2,6 +2,8 @@
namespace Tests\Feature\Controller\Admin;
+use App\Auth\SecondFactor;
+use App\Sku;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
@@ -147,6 +149,45 @@
}
/**
+ * Test reseting 2FA (POST /api/v4/users/<user-id>/reset2FA)
+ */
+ public function testReset2FA(): void
+ {
+ $user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+
+ $sku2fa = Sku::firstOrCreate(['title' => '2fa']);
+ $user->assignSku($sku2fa);
+ SecondFactor::seed('userscontrollertest1@userscontroller.com');
+
+ // Test unauthorized access to admin API
+ $response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/reset2FA", []);
+ $response->assertStatus(403);
+
+ $entitlements = $user->fresh()->entitlements()->where('sku_id', $sku2fa->id)->get();
+ $this->assertCount(1, $entitlements);
+
+ $sf = new SecondFactor($user);
+ $this->assertCount(1, $sf->factors());
+
+ // Test reseting 2FA
+ $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/reset2FA", []);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("2-Factor authentication reset successfully.", $json['message']);
+ $this->assertCount(2, $json);
+
+ $entitlements = $user->fresh()->entitlements()->where('sku_id', $sku2fa->id)->get();
+ $this->assertCount(0, $entitlements);
+
+ $sf = new SecondFactor($user);
+ $this->assertCount(0, $sf->factors());
+ }
+
+ /**
* Test user suspending (POST /api/v4/users/<user-id>/suspend)
*/
public function testSuspend(): void
diff --git a/src/tests/Feature/EntitlementTest.php b/src/tests/Feature/EntitlementTest.php
--- a/src/tests/Feature/EntitlementTest.php
+++ b/src/tests/Feature/EntitlementTest.php
@@ -2,7 +2,6 @@
namespace Tests\Feature;
-use App\Auth\SecondFactor;
use App\Domain;
use App\Entitlement;
use App\Package;

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 8:16 PM (18 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18826280
Default Alt Text
D1492.1775247363.diff (12 KB)

Event Timeline