diff --git a/Taskfile.yml b/Taskfile.yml index 5f75072..2c43997 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -16,10 +16,14 @@ tasks: db:seed: desc: Seed the Auth module database - cmd: php artisan modules:seed --module=auth + cmd: '{{.APP}} php artisan modules:seed --module=auth' + + make-admin: + desc: Promote an user to Admin + cmd: '{{.APP}} php artisan auth:make-admin' # ── Code Generation ──────────────────────────────────────────── types:generate: desc: Generate TypeScript types from PHP DTOs and enums - cmd: php artisan module:generate-types auth + cmd: '{{.APP}} php artisan module:generate-types auth' diff --git a/database/seeders/AuthDatabaseSeeder.php b/database/seeders/AuthDatabaseSeeder.php index 9447c4a..ba49583 100644 --- a/database/seeders/AuthDatabaseSeeder.php +++ b/database/seeders/AuthDatabaseSeeder.php @@ -12,7 +12,7 @@ class AuthDatabaseSeeder extends Seeder */ public function run(): void { - $adminUser = User::firstOrCreate( + $user = User::firstOrCreate( ['email' => 'chef@saucebase.dev'], [ 'name' => 'Admin Chef', @@ -20,19 +20,6 @@ public function run(): void ] ); - // Assign the admin role to the admin user - $adminUser->assignRole('admin'); - - // Create test users for E2E tests - $user = User::firstOrCreate( - ['email' => 'test@example.com'], - [ - 'name' => 'Test User', - 'password' => bcrypt('secretsauce'), - 'email_verified_at' => now(), - ] - ); - $user->assignRole('user'); } } diff --git a/src/Console/Commands/MakeAdminCommand.php b/src/Console/Commands/MakeAdminCommand.php new file mode 100644 index 0000000..56ca4ad --- /dev/null +++ b/src/Console/Commands/MakeAdminCommand.php @@ -0,0 +1,37 @@ +argument('email') ?? text('Email address', required: true); + + $user = User::where('email', $email)->first(); + + if (! $user) { + $this->error("No user found with email: {$email}"); + $this->line('Register an account first, then re-run this command.'); + + return self::FAILURE; + } + + $user->syncRoles([Role::ADMIN->value]); + + $this->info("Promoted admin: {$user->name} <{$user->email}>"); + + return self::SUCCESS; + } +} diff --git a/tests/Feature/MakeAdminCommandTest.php b/tests/Feature/MakeAdminCommandTest.php new file mode 100644 index 0000000..7367fcb --- /dev/null +++ b/tests/Feature/MakeAdminCommandTest.php @@ -0,0 +1,53 @@ +createUser(); + + $this->artisan('auth:make-admin', ['email' => $user->email]) + ->assertSuccessful() + ->expectsOutputToContain('Promoted admin'); + + $this->assertTrue($user->fresh()->hasRole(Role::ADMIN)); + } + + public function test_fails_when_user_not_found(): void + { + $this->artisan('auth:make-admin', ['email' => 'nobody@example.com']) + ->assertFailed() + ->expectsOutputToContain('No user found'); + } + + public function test_replaces_existing_role_with_admin(): void + { + $user = $this->createUser(); + $user->assignRole(Role::USER); + + $this->artisan('auth:make-admin', ['email' => $user->email]) + ->assertSuccessful(); + + $this->assertTrue($user->fresh()->hasRole(Role::ADMIN)); + $this->assertFalse($user->fresh()->hasRole(Role::USER)); + } + + public function test_is_idempotent(): void + { + $user = $this->createUser(); + + $this->artisan('auth:make-admin', ['email' => $user->email])->assertSuccessful(); + $this->artisan('auth:make-admin', ['email' => $user->email])->assertSuccessful(); + + $this->assertCount(1, $user->fresh()->roles); + $this->assertTrue($user->fresh()->hasRole(Role::ADMIN)); + } +} diff --git a/tests/e2e/tests/login/login.security.spec.ts b/tests/e2e/tests/login/login.security.spec.ts index 9e4db8a..c06cadc 100644 --- a/tests/e2e/tests/login/login.security.spec.ts +++ b/tests/e2e/tests/login/login.security.spec.ts @@ -2,6 +2,8 @@ import { test, expect } from '@e2e/fixtures'; import { LoginPage } from '../../pages/LoginPage'; test.describe('Login Security', () => { + test.describe.configure({ mode: 'serial' }); + let loginPage: LoginPage; test.beforeEach(async ({ page }) => { @@ -10,8 +12,7 @@ test.describe('Login Security', () => { }); test.describe('Rate Limiting', () => { - test.describe.configure({ mode: 'serial' }); - + test('blocks login after too many failed attempts', async () => { const invalidUser = { email: 'invalid@example.com', password: 'wrongpassword' };