Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117792704
D1492.1775258596.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
12 KB
Referenced Files
None
Subscribers
None
D1492.1775258596.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
@@ -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">
¹ 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">×</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
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 11:23 PM (13 h, 42 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18826280
Default Alt Text
D1492.1775258596.diff (12 KB)
Attached To
Mode
D1492: Allow admins to reset 2FA for users
Attached
Detach File
Event Timeline