Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add admin management via web #2933

Merged
merged 5 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions sourcecode/hub/app/Http/Controllers/Admin/AdminsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Admin;

use App\Http\Requests\GrantAdminRequest;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;

use function redirect;
use function response;

final readonly class AdminsController
{
public function index(): Response
{
$users = User::where('admin', true)->paginate();

return response()->view('admin.admins.index', [
'users' => $users,
]);
}

public function add(GrantAdminRequest $request): RedirectResponse
{
$email = $request->validated('email');

$user = User::where('email', $email)
->where('email_verified', true)
->firstOrFail();
$user->admin = true;
$user->save();

return redirect()->back();
}

public function remove(User $user): RedirectResponse
{
$user->admin = false;
$user->save();

return redirect()->back();
}
}
21 changes: 21 additions & 0 deletions sourcecode/hub/app/Http/Requests/GrantAdminRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests;

use App\Rules\VerifiedUserEmail;
use Illuminate\Foundation\Http\FormRequest;

class GrantAdminRequest extends FormRequest
{
/**
* @return array<mixed>
*/
public function rules(VerifiedUserEmail $verifiedUserEmail): array
{
return [
'email' => ['required', 'email', $verifiedUserEmail],
];
}
}
30 changes: 30 additions & 0 deletions sourcecode/hub/app/Rules/VerifiedUserEmail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace App\Rules;

use App\Models\User;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

/**
* Ensure an email belongs to a user with a verified email address.
*/
class VerifiedUserEmail implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$user = User::where('email', $value)->first();

if ($user === null) {
$fail('No user with that email address');

return;
}

if (!$user->email_verified) {
$fail('User does not have a verified email address');
}
}
}
1 change: 1 addition & 0 deletions sourcecode/hub/lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,5 @@
'start-job' => 'Start job',
'danger-zone' => 'Danger zone',
'confirm-reindex' => 'Reindexing will make content listings unavailable until the process has completed. Are you sure you want to continue?',
'admins' => 'Admins',
];
33 changes: 33 additions & 0 deletions sourcecode/hub/resources/views/admin/admins/index.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<x-layout>
<x-slot:title>{{ trans('messages.admins') }}</x-slot:title>

<table class="table table-borderless table-striped">
<thead>
</thead>

<tbody>
@foreach ($users as $user)
<tr>
<td>{{ $user->email }}</td>
<td>
<x-form action="{{ route('admin.admins.remove', [$user]) }}" method="DELETE">
<x-form.button class="btn-danger btn-sm">
{{ trans('messages.remove') }}
</x-form.button>
</x-form>
</td>
</tr>
@endforeach
</tbody>
</table>

{{ $users->links() }}

<hr>

<x-form action="{{ route('admin.admins.add') }}">
<x-form.field name="email" :label="trans('messages.email-address')" />

<x-form.button class="btn-primary">{{ trans('messages.add') }}</x-form.button>
</x-form>
</x-layout>
6 changes: 6 additions & 0 deletions sourcecode/hub/resources/views/admin/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
{{ trans('messages.attach-context-to-contents') }}
</a>
</li>

<li>
<a href="{{ route('admin.admins.index') }}">
{{ trans('messages.admins') }}
</a>
</li>
</ul>

<h3>{{ trans('messages.danger-zone') }}</h3>
Expand Down
13 changes: 13 additions & 0 deletions sourcecode/hub/routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use App\Http\Controllers\Admin\AdminController;
use App\Http\Controllers\Admin\AdminsController;
use App\Http\Controllers\Admin\ContextController;
use App\Http\Controllers\Admin\LtiPlatformController;
use App\Http\Controllers\Admin\LtiToolController;
Expand Down Expand Up @@ -268,6 +269,18 @@
Route::post('/rebuild-content-index', [AdminController::class, 'rebuildContentIndex'])
->name('admin.rebuild-content-index');

Route::get('/admins')
->uses([AdminsController::class, 'index'])
->name('admin.admins.index');

Route::post('/admins')
->uses([AdminsController::class, 'add'])
->name('admin.admins.add');

Route::delete('/admins/{user}')
->uses([AdminsController::class, 'remove'])
->name('admin.admins.remove');

Route::get('/contexts')
->uses([ContextController::class, 'index'])
->name('admin.contexts.index');
Expand Down
90 changes: 90 additions & 0 deletions sourcecode/hub/tests/Browser/AdminTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,94 @@ public function testCanAddContextToLtiPlatform(): void
),
);
}

public function testListsAdmins(): void
{
User::factory()->withEmail('admin@edlib.test')->admin()->create();
User::factory()->withEmail('nimda@bilde.test')->admin()->create();
User::factory()->withEmail('luser@example.com')->create();

$this->browse(
fn(Browser $browser) => $browser
->loginAs('admin@edlib.test')
->assertAuthenticated()
->visit('/admin/admins')
->with(
'main table',
fn(Browser $table) => $table
->assertSee('admin@edlib.test')
->assertSee('nimda@bilde.test')
->assertDontSee('luser@example.com'),
),
);
}

public function testAddsAdmins(): void
{
User::factory()->withEmail('admin@edlib.test')->admin()->create();
User::factory()->withEmail('nimda@bilde.test')->create();

$this->browse(
fn(Browser $browser) => $browser
->loginAs('admin@edlib.test')
->assertAuthenticated()
->visit('/admin/admins')
->assertDontSeeIn('main table', 'nimda@bilde.test')
->type('email', 'nimda@bilde.test')
->press('Add')
->assertSeeIn('main table', 'nimda@bilde.test'),
);
}

public function testEmailOfAddedAdminMustBelongToExistingUser(): void
{
User::factory()->withEmail('admin@edlib.test')->admin()->create();

$this->browse(
fn(Browser $browser) => $browser
->loginAs('admin@edlib.test')
->assertAuthenticated()
->visit('/admin/admins')
->type('email', 'nimda@bilde.test')
->press('Add')
->assertDontSeeIn('main table', 'nimda@bilde.test')
->assertSeeIn('.invalid-feedback', 'No user with that email address'),
);
}

public function testEmailOfAddedAdminMustBeVerified(): void
{
User::factory()->withEmail('admin@edlib.test')->admin()->create();
User::factory()->withEmail('nimda@bilde.test', verified: false)->create();

$this->browse(
fn(Browser $browser) => $browser
->loginAs('admin@edlib.test')
->assertAuthenticated()
->visit('/admin/admins')
->type('email', 'nimda@bilde.test')
->press('Add')
->assertDontSeeIn('main table', 'nimda@bilde.test')
->assertSeeIn('.invalid-feedback', 'User does not have a verified email address'),
);
}

public function testRemovesAdmins(): void
{
User::factory()->withEmail('admin@edlib.test')->admin()->create();

$this->browse(
fn(Browser $browser) => $browser
->loginAs('admin@edlib.test')
->assertAuthenticated()
->visit('/admin/admins')
->with(
'main table',
fn(Browser $table) => $table
->assertSee('admin@edlib.test')
->press('Remove'),
)
->assertTitleContains('Forbidden'),
);
}
}
9 changes: 9 additions & 0 deletions sourcecode/hub/tests/Feature/Admin/AdminTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,13 @@ public function testNonAdminsCannotUseAdminEndpoints(): void
->get('/content/create/the-tool/the-extra')
->assertForbidden();
}

public function testCannotAccessAdminsAsNonAdmin(): void
{
$user = User::factory()->create();

$this->actingAs($user)
->get('/admin/admins')
->assertForbidden();
}
}