feat(admin): add first-admin bootstrap command#37
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a CLI bootstrap path for establishing the first admin account using Silber Bouncer roles, along with Pest coverage for the main success/failure flows.
Changes:
- Introduces
app:bootstrap-first-adminArtisan command to promote the first user or a specific user by email to theadminrole. - Ensures the
adminrole exists before assignment. - Adds Pest feature tests covering promote-first-user, promote-by-email, and missing-email-user failure.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| app/Console/Commands/BootstrapFirstAdmin.php | Implements the bootstrap/promote command and role creation guard. |
| tests/Feature/Admin/BootstrapFirstAdminCommandTest.php | Adds feature tests for the command’s primary flows. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| protected function createFirstAdmin(): int | ||
| { | ||
| $user = User::query()->first(); | ||
|
|
||
| if ($user === null) { | ||
| $this->components->error('No users exist yet. Create a user first, then promote it with the email argument.'); | ||
|
|
||
| return self::FAILURE; | ||
| } | ||
|
|
||
| $this->ensureAdminRoleExists(); | ||
|
|
||
| if ($user->isAn('admin')) { | ||
| $this->components->info("The first user, {$user->email}, is already an admin."); | ||
|
|
||
| return self::SUCCESS; | ||
| } | ||
|
|
||
| $user->assign('admin'); |
There was a problem hiding this comment.
createFirstAdmin() will promote the first user even if an admin already exists (e.g., the second user is already an admin). That can unexpectedly grant additional admin access on subsequent runs and undermines the “first admin bootstrap” safety goal. Consider first checking whether any user already has the admin role and, if so, exiting without changing roles (or requiring the email argument).
|
|
||
| protected function createFirstAdmin(): int | ||
| { | ||
| $user = User::query()->first(); |
There was a problem hiding this comment.
User::query()->first() has no deterministic ordering, so “first user” may vary by database engine/query plan. If this command is meant to target the earliest-created user, explicitly order by a stable column (e.g., id or created_at) before selecting the first record.
| $user = User::query()->first(); | |
| $user = User::query()->orderBy('id')->first(); |
| #[Signature('app:bootstrap-first-admin {email? : Existing user email to promote}')] | ||
| #[Description('Create or promote the first admin user')] | ||
| class BootstrapFirstAdmin extends Command | ||
| { | ||
| public function handle(): int | ||
| { | ||
| $email = $this->argument('email'); | ||
|
|
||
| if (is_string($email) && $email !== '') { | ||
| return $this->promoteExistingUser($email); | ||
| } | ||
|
|
||
| return $this->createFirstAdmin(); | ||
| } | ||
|
|
||
| protected function createFirstAdmin(): int | ||
| { |
There was a problem hiding this comment.
The command description and method name suggest a “create” flow, but the implementation only promotes an existing user (either the first user or a user by email). To avoid confusing operators, consider updating the description text and/or renaming createFirstAdmin() to reflect the actual behavior (or implement the missing create path if intended).
| it('promotes the first user when no admin exists', function (): void { | ||
| $user = User::factory()->create(); | ||
|
|
||
| $this->artisan('app:bootstrap-first-admin') | ||
| ->assertExitCode(0); | ||
|
|
||
| expect($user->fresh()->isAn('admin'))->toBeTrue(); | ||
| }); |
There was a problem hiding this comment.
Test coverage currently exercises “promote first user”, “promote by email”, and “missing user by email”, but it doesn’t cover the bootstrap safety cases introduced in the command: (1) no users exist (should fail), and (2) an admin already exists but is not the first user (should not silently grant admin to the first user). Adding focused tests for these cases would help prevent privilege-escalation regressions.
Closes #12\n\nSummary:\n- Adds an Artisan command to create or promote the first admin user\n- Ensures the admin role exists before assignment\n- Supports both bootstrap and email-based promotion flows\n- Adds focused Pest coverage for create, promote, and failure paths\n\nValidation:\n- vendor/bin/pint --dirty --format agent\n- php artisan test --compact\n- npm run build