+ @endif
- @if(isset($response))
- @php( App\Helpers\Fixometer::printResponse($response) )
- @endif
-
-
-
-
-
-
-
-
-
- | Name |
- Email address |
- Role |
- Location |
- Country |
- Groups |
- Joined |
- Last login |
-
-
-
- @foreach($userlist as $u)
-
- @php( $display = true )
-
- permissions, 'idpermissions');
- if(!in_array($p, $user_permissions)) {
- $display = false;
- break;
- }
- }
- }
- ?>
-
- @if ($display)
-
- |
-
- @if(App\Helpers\Fixometer::hasRole($user, 'Administrator'))
- {{ $u->name }}
- @else
- {{ $u->name }}
- @endif
-
- |
-
-
- {{ Str::limit($u->email, 15) }}
-
- |
-
- @lang($u->role)
- |
-
- @if (!empty($u->location))
- location; ?>
- @else
- N/A
- @endif
- |
- {{ \App\Helpers\Fixometer::getCountryFromCountryCode($u->country_code) }} |
-
- @if (isset($u->groups) && $u->groups->count() > 0)
- {{ $u->groups->count() }}
- @else
- 0
- @endif
- |
-
-
- {{ !is_null($u->created_at) ? $u->created_at->diffForHumans(null, true) : 'Never' }}
- |
-
-
- {{ !is_null($u->lastLogin) ? $u->lastLogin->diffForHumans(null, true) : 'Never' }}
- |
-
- @endif
- @endforeach
-
-
-
-
-
-
-
-
- Showing {{ $userlist->firstItem() }} to {{ $userlist->lastItem() }} of {{ $userlist->total() }} results
-
-
-
-
-
-
+ @if (\Session::has('danger'))
+
+
+ {!! \Session::get('danger') !!}
+ @endif
+
+ $name) {
+ $countries[] = ['code' => $code, 'name' => $name];
+ }
+ $roles = [];
+ foreach (App\Helpers\Fixometer::allRoles() as $r) {
+ $roles[] = ['id' => $r->idroles, 'name' => $r->role];
+ }
+ $can_edit = App\Helpers\Fixometer::hasRole(Auth::user(), 'Administrator');
+ ?>
+
+
+
@lang('partials.loading')...
+
+
+
-
-@include('includes/modals/create-user')
+ @include('includes/modals/create-user')
@endsection
diff --git a/routes/api.php b/routes/api.php
index 5564ad0dc..acb9b15c8 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -93,6 +93,12 @@
Route::patch('{id}', [API\EventController::class, 'updateEventv2']);
});
+ Route::prefix('/users')->group(function() {
+ Route::middleware('auth:api')->group(function() {
+ Route::get('', [API\UserController::class, 'listUsersv2']);
+ });
+ });
+
Route::prefix('/networks')->group(function() {
Route::get('/', [API\NetworkController::class, 'getNetworksv2']);
Route::get('{id}', [API\NetworkController::class, 'getNetworkv2']);
diff --git a/tests/Feature/Admin/Users/ViewUsersTest.php b/tests/Feature/Admin/Users/ViewUsersTest.php
index 6bba57f16..4b5ee2f48 100644
--- a/tests/Feature/Admin/Users/ViewUsersTest.php
+++ b/tests/Feature/Admin/Users/ViewUsersTest.php
@@ -3,135 +3,26 @@
namespace Tests\Feature;
use App\User;
-use Carbon\Carbon;
-use DB;
-use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ViewUsersTest extends TestCase
{
- protected function setUp(): void
+ public function testAdminCanLoadUsersPage(): void
{
- parent::setUp();
- DB::statement('SET foreign_key_checks=0');
- User::truncate();
- DB::statement('SET foreign_key_checks=1');
-
- // Given we're logged in as an admin
$admin = User::factory()->administrator()->create();
$this->actingAs($admin);
- }
-
- /** @test */
- public function an_admin_can_view_list_of_users(): void
- {
- // Given we have users in the database
- $users = User::factory()->count(10)->create();
- // When we visit the list of users
$response = $this->get('/user/all');
-
- // Then the users should be in the list
- $response->assertSeeText($users[0]->name);
+ $response->assertSuccessful();
+ $response->assertSee('
count(41)->create();
+ $user = User::factory()->restarter()->create();
+ $this->actingAs($user);
- // When we visit the list of users
$response = $this->get('/user/all');
-
- // Then we should see the count of users
- $response->assertSeeText(42);
- }
-
- /** @test */
- public function admin_can_see_users_last_login_time(): void
- {
- // Given we have a user who has just logged in
- $lastLogin = new \Carbon\Carbon();
- $user = User::factory()->create([
- 'updated_at' => $lastLogin,
- ]);
-
- // When we visit the list of users
- $response = $this->get('/user/all');
-
- // Then we should see the last login date for that user
- $response->assertSeeText($lastLogin->diffForHumans(null, true));
- }
-
- /** @test */
- public function admin_can_see_users_last_login_time_on_filtered_results(): void
- {
- // Given we have a user who has just logged in
- $lastLogin = new \Carbon\Carbon();
- $user = User::factory()->create([
- 'updated_at' => $lastLogin,
- ]);
-
- // When we visit the list of users and filter by that user
- $response = $this->get('/user/all/search?name='.$user->name);
-
- // Then we should see the last login date for that user
- $response->assertSeeText($lastLogin->diffForHumans(null, true));
- }
-
- /** @test */
- public function admin_can_sort_user_list_by_last_login(): void
- {
- // Given we have users with various login times
- $dateOfMostRecentLogin = new Carbon();
- $dateOfLeastRecentLogin = new Carbon('-1 year');
-
- $userWithMostRecentLogin = User::factory()->create([
- 'last_login_at' => $dateOfMostRecentLogin,
- ]);
- $otherUsers = User::factory()->count(42)->create([
- 'last_login_at' => new Carbon('-1 month'),
- ]);
- $userWithLeastRecentLogin = User::factory()->create([
- 'last_login_at' => $dateOfLeastRecentLogin,
- ]);
-
- // Try this a few times because we might be unlucky and hit a second boundary.
- $found = false;
-
- for ($i = 0; $i < 10; $i++) {
- // When we visit the list of users and sort it by last login descending
- $response = $this->get('/user/all/search?sort=last_login_at&sortdir=desc');
- $datestr = $dateOfMostRecentLogin->diffForHumans(null, true);
-
- if (stripos($response->getContent(), $datestr) !== false) {
- // Then the first result is the most recent login
- $found = true;
- break;
- }
- }
-
- $this->assertTrue($found);
-
- // When we visit the list of users and sort it by last login descending
- $response = $this->get('/user/all/search?sort=last_login_at&sortdir=asc');
-
- // Then the first result is the most recent login
- $found = false;
-
- for ($i = 0; $i < 10; $i++) {
- // When we visit the list of users and sort it by last login descending
- $response = $this->get('/user/all/search?sort=last_login_at&sortdir=asc');
- $datestr = $dateOfLeastRecentLogin->diffForHumans(null, true);
-
- if (stripos($response->getContent(), $datestr) !== false) {
- // Then the first result is the most recent login
- $found = true;
- break;
- }
- }
-
- $this->assertTrue($found);
+ $response->assertRedirect('/user/forbidden');
}
}
diff --git a/tests/Feature/Users/APIv2UsersTest.php b/tests/Feature/Users/APIv2UsersTest.php
new file mode 100644
index 000000000..29f3b2b3b
--- /dev/null
+++ b/tests/Feature/Users/APIv2UsersTest.php
@@ -0,0 +1,125 @@
+withExceptionHandling();
+ }
+
+ public function testListUsersRequiresAuth(): void
+ {
+ $response = $this->getJson('/api/v2/users');
+ $response->assertStatus(401);
+ }
+
+ public function testListUsersForbiddenForNonAdmin(): void
+ {
+ $user = User::factory()->restarter()->create(['api_token' => 'r1']);
+ $this->actingAs($user);
+
+ $response = $this->getJson('/api/v2/users?api_token=r1');
+ $response->assertStatus(403);
+ }
+
+ public function testListUsersAsAdminReturnsPaginated(): void
+ {
+ $admin = User::factory()->administrator()->create(['api_token' => 'admin1']);
+ User::factory()->host()->create(['name' => 'Aaa Host']);
+ User::factory()->restarter()->create(['name' => 'Bbb Restarter']);
+ $this->actingAs($admin);
+
+ $response = $this->getJson('/api/v2/users?api_token=admin1');
+ $response->assertSuccessful();
+
+ $payload = $response->json();
+ $this->assertArrayHasKey('data', $payload);
+ $this->assertArrayHasKey('meta', $payload);
+ $this->assertArrayHasKey('current_page', $payload['meta']);
+ $this->assertArrayHasKey('total', $payload['meta']);
+ $this->assertIsArray($payload['data']);
+
+ foreach ($payload['data'] as $row) {
+ $this->assertArrayHasKey('id', $row);
+ $this->assertArrayHasKey('name', $row);
+ $this->assertArrayHasKey('email', $row);
+ $this->assertArrayHasKey('role', $row);
+ $this->assertArrayHasKey('role_name', $row);
+ $this->assertArrayHasKey('location', $row);
+ $this->assertArrayHasKey('country', $row);
+ $this->assertArrayHasKey('country_name', $row);
+ $this->assertArrayHasKey('groups_count', $row);
+ $this->assertArrayHasKey('created_at', $row);
+ $this->assertArrayHasKey('last_login_at', $row);
+ }
+ }
+
+ public function testListUsersFilterByName(): void
+ {
+ $admin = User::factory()->administrator()->create(['api_token' => 'admin1']);
+ User::factory()->host()->create(['name' => 'Alpha Person']);
+ User::factory()->host()->create(['name' => 'Bravo Person']);
+ $this->actingAs($admin);
+
+ $response = $this->getJson('/api/v2/users?api_token=admin1&name=Alpha');
+ $response->assertSuccessful();
+
+ $names = array_column($response->json('data'), 'name');
+ $this->assertContains('Alpha Person', $names);
+ $this->assertNotContains('Bravo Person', $names);
+ }
+
+ public function testListUsersFilterByEmail(): void
+ {
+ $admin = User::factory()->administrator()->create(['api_token' => 'admin1']);
+ User::factory()->host()->create(['email' => 'unique-target@example.com']);
+ User::factory()->host()->create(['email' => 'noise@example.com']);
+ $this->actingAs($admin);
+
+ $response = $this->getJson('/api/v2/users?api_token=admin1&email=unique-target');
+ $response->assertSuccessful();
+
+ $emails = array_column($response->json('data'), 'email');
+ $this->assertContains('unique-target@example.com', $emails);
+ $this->assertNotContains('noise@example.com', $emails);
+ }
+
+ public function testListUsersFilterByRole(): void
+ {
+ $admin = User::factory()->administrator()->create(['api_token' => 'admin1']);
+ $host = User::factory()->host()->create();
+ User::factory()->restarter()->create();
+ $this->actingAs($admin);
+
+ $response = $this->getJson('/api/v2/users?api_token=admin1&role=' . Role::HOST);
+ $response->assertSuccessful();
+
+ $roles = array_unique(array_column($response->json('data'), 'role'));
+ $this->assertEquals([Role::HOST], array_values($roles));
+ }
+
+ public function testListUsersSortByNameAsc(): void
+ {
+ $admin = User::factory()->administrator()->create(['api_token' => 'admin1']);
+ User::factory()->host()->create(['name' => 'Zeta Sortable']);
+ User::factory()->host()->create(['name' => 'Alpha Sortable']);
+ $this->actingAs($admin);
+
+ $response = $this->getJson('/api/v2/users?api_token=admin1&name=Sortable&sort=name&sortdir=asc');
+ $response->assertSuccessful();
+
+ $names = array_column($response->json('data'), 'name');
+ $alphaIdx = array_search('Alpha Sortable', $names);
+ $zetaIdx = array_search('Zeta Sortable', $names);
+ $this->assertNotFalse($alphaIdx);
+ $this->assertNotFalse($zetaIdx);
+ $this->assertLessThan($zetaIdx, $alphaIdx);
+ }
+}
diff --git a/tests/Feature/Users/UserAdminTest.php b/tests/Feature/Users/UserAdminTest.php
index 1ecddc0db..12123ac83 100644
--- a/tests/Feature/Users/UserAdminTest.php
+++ b/tests/Feature/Users/UserAdminTest.php
@@ -43,10 +43,11 @@ public function testUsersPage($role, $cansee): void
$response = $this->get('/user/all');
if ($cansee) {
- $response->assertSee('Create new user');
- $response->assertSee($admin->name);
+ $response->assertSuccessful();
+ $response->assertSee('assertDontSee('Create new user');
+ // Non-admin is redirected away from the page in the new Vue flow.
+ $response->assertRedirect('/user/forbidden');
}
}
diff --git a/tests/Integration/admin-users.test.js b/tests/Integration/admin-users.test.js
new file mode 100644
index 000000000..e463d5e7b
--- /dev/null
+++ b/tests/Integration/admin-users.test.js
@@ -0,0 +1,27 @@
+/**
+ * Smoke-test coverage for /user/all migrated from a server-rendered blade
+ * to the UsersPage Vue SPA backed by GET /api/v2/users.
+ */
+
+const { test } = require('./fixtures')
+const { expect } = require('@playwright/test')
+const { login } = require('./utils')
+
+const ADMIN_EMAIL = 'jane@bloggs.net'
+const RESTARTER_EMAIL = 'host@test.net'
+const PASSWORD = 'passw0rd'
+
+test('Admin /user/all renders the UsersPage Vue SPA', async ({ page, baseURL }) => {
+ test.slow()
+ await login(page, baseURL, ADMIN_EMAIL, PASSWORD)
+ await page.goto(baseURL + '/user/all')
+ await expect(page.locator('[data-testid="users-table"]')).toBeVisible({ timeout: 10000 })
+ await expect(page.locator('[data-testid="users-filter-submit"]')).toBeVisible()
+})
+
+test('Non-admin is redirected away from /user/all', async ({ page, baseURL }) => {
+ test.slow()
+ await login(page, baseURL, RESTARTER_EMAIL, PASSWORD)
+ await page.goto(baseURL + '/user/all')
+ await expect(page).toHaveURL(/\/user\/forbidden$/)
+})