Skip to content

feat(admin): add first-admin bootstrap command#37

Merged
ibourgeois merged 1 commit into
mainfrom
codex/12-first-admin-command
Apr 10, 2026
Merged

feat(admin): add first-admin bootstrap command#37
ibourgeois merged 1 commit into
mainfrom
codex/12-first-admin-command

Conversation

@ibourgeois
Copy link
Copy Markdown
Contributor

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

Copilot AI review requested due to automatic review settings April 10, 2026 10:28
@ibourgeois ibourgeois merged commit e2957cd into main Apr 10, 2026
3 checks passed
@ibourgeois ibourgeois deleted the codex/12-first-admin-command branch April 10, 2026 10:29
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-admin Artisan command to promote the first user or a specific user by email to the admin role.
  • Ensures the admin role 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.

Comment on lines +26 to +44
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');
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.

protected function createFirstAdmin(): int
{
$user = User::query()->first();
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$user = User::query()->first();
$user = User::query()->orderBy('id')->first();

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +27
#[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
{
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +15
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();
});
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(admin): add first-admin bootstrap command

2 participants