diff --git a/app/Domains/Auth/Models/User.php b/app/Domains/Auth/Models/User.php index d502e04..329af25 100644 --- a/app/Domains/Auth/Models/User.php +++ b/app/Domains/Auth/Models/User.php @@ -53,6 +53,16 @@ class User extends Authenticatable return $this->belongsTo(Role::class, 'rol_codigo', 'codigo'); } + public function hasPermission(string $permissionCode): bool + { + return $this->role() + ->whereHas( + 'permissions', + fn ($query) => $query->where('permisos.codigo', $permissionCode) + ) + ->exists(); + } + /** * @return BelongsTo */ diff --git a/app/Domains/Auth/Services/PasswordLoginService.php b/app/Domains/Auth/Services/PasswordLoginService.php index 6915322..1e25389 100644 --- a/app/Domains/Auth/Services/PasswordLoginService.php +++ b/app/Domains/Auth/Services/PasswordLoginService.php @@ -5,6 +5,7 @@ namespace App\Domains\Auth\Services; use App\Domains\Auth\Exceptions\AccountLockedException; use App\Domains\Auth\Models\LoginAttempt; use App\Domains\Auth\Models\User; +use App\Domains\Authorization\Enums\PermissionCode; use App\Domains\Authorization\Enums\RoleCode; use Carbon\CarbonImmutable; use Illuminate\Support\Facades\DB; @@ -63,7 +64,7 @@ class PasswordLoginService } /** - * Authenticate a tenant-bound scanner without requiring its tenant code. + * Authenticate a tenant-bound user authorized to scan tickets. * * @throws AccountLockedException * @throws ValidationException @@ -80,8 +81,9 @@ class PasswordLoginService null, $ipAddress, $userAgent, - RoleCode::Scanner, + null, true, + PermissionCode::ScanTickets->value, ); } @@ -91,8 +93,9 @@ class PasswordLoginService ?string $tenantCode, ?string $ipAddress, ?string $userAgent, - RoleCode $requiredRole = RoleCode::User, + ?RoleCode $requiredRole = RoleCode::User, bool $requiresTenant = false, + ?string $requiredPermission = null, ): User { $normalizedEmail = mb_strtolower(trim($email)); $now = CarbonImmutable::now(); @@ -107,10 +110,21 @@ class PasswordLoginService $now, $requiredRole, $requiresTenant, + $requiredPermission, ): array { $user = User::query() ->where('email', $normalizedEmail) - ->where('rol_codigo', $requiredRole->value) + ->when( + $requiredRole !== null, + fn ($query) => $query->where('rol_codigo', $requiredRole->value), + ) + ->when( + $requiredPermission !== null, + fn ($query) => $query->whereHas( + 'role.permissions', + fn ($query) => $query->where('permisos.codigo', $requiredPermission) + ), + ) ->when( $requiresTenant, fn ($query) => $query->whereNotNull('tenant_codigo'), diff --git a/app/Domains/Auth/Services/ScannerContextService.php b/app/Domains/Auth/Services/ScannerContextService.php index 970fcee..b4acbb3 100644 --- a/app/Domains/Auth/Services/ScannerContextService.php +++ b/app/Domains/Auth/Services/ScannerContextService.php @@ -3,7 +3,6 @@ namespace App\Domains\Auth\Services; use App\Domains\Auth\Models\User; -use App\Domains\Authorization\Enums\RoleCode; class ScannerContextService { @@ -13,7 +12,7 @@ class ScannerContextService ->with([ 'menues' => fn ($query) => $query->whereHas( 'roles', - fn ($query) => $query->where('codigo', RoleCode::Scanner->value) + fn ($query) => $query->where('codigo', $user->rol_codigo) ), ]) ->firstOrFail(); diff --git a/app/Domains/Authorization/Enums/PermissionCode.php b/app/Domains/Authorization/Enums/PermissionCode.php new file mode 100644 index 0000000..fb520d0 --- /dev/null +++ b/app/Domains/Authorization/Enums/PermissionCode.php @@ -0,0 +1,8 @@ +rol_codigo !== RoleCode::Scanner->value || ! $user->tenant_codigo + || ! $user->hasPermission(PermissionCode::ScanTickets->value) ) { throw new AuthorizationException; } diff --git a/database/seeders/AuthorizationSeeder.php b/database/seeders/AuthorizationSeeder.php index 212778c..344f05d 100644 --- a/database/seeders/AuthorizationSeeder.php +++ b/database/seeders/AuthorizationSeeder.php @@ -2,6 +2,7 @@ namespace Database\Seeders; +use App\Domains\Authorization\Enums\PermissionCode; use App\Domains\Authorization\Enums\RoleCode; use App\Domains\Authorization\Models\Permission; use App\Domains\Authorization\Models\Role; @@ -81,7 +82,7 @@ class AuthorizationSeeder extends Seeder 'nombre' => 'Gestionar tickets', 'descripcion' => 'Permite emitir, invalidar o regenerar tickets.', ], - 'tickets.escanear' => [ + PermissionCode::ScanTickets->value => [ 'nombre' => 'Escanear tickets', 'descripcion' => 'Permite validar y consumir tickets de las categorías asignadas al usuario.', ], @@ -119,12 +120,12 @@ class AuthorizationSeeder extends Seeder RoleCode::AdminApp->value => [ 'nombre' => 'Administrador de la aplicación', 'descripcion' => 'Accede a los menús administrativos de la aplicación.', - 'permisos' => [], + 'permisos' => [PermissionCode::ScanTickets->value], ], RoleCode::Scanner->value => [ 'nombre' => 'Scanner', 'descripcion' => 'Valida y consume tickets de las categorías que tiene asignadas.', - 'permisos' => ['tickets.escanear'], + 'permisos' => [PermissionCode::ScanTickets->value], ], RoleCode::User->value => [ 'nombre' => 'Usuario', diff --git a/tests/Feature/Auth/ScannerLoginControllerTest.php b/tests/Feature/Auth/ScannerLoginControllerTest.php index 805e16c..b33b2a9 100644 --- a/tests/Feature/Auth/ScannerLoginControllerTest.php +++ b/tests/Feature/Auth/ScannerLoginControllerTest.php @@ -3,7 +3,9 @@ namespace Tests\Feature\Auth; use App\Domains\Auth\Models\User; +use App\Domains\Authorization\Enums\PermissionCode; use App\Domains\Authorization\Enums\RoleCode; +use App\Domains\Authorization\Models\Permission; use App\Domains\Authorization\Models\Role; use App\Domains\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -14,12 +16,17 @@ class ScannerLoginControllerTest extends TestCase { use RefreshDatabase; - public function test_it_logs_in_a_tenant_bound_scanner(): void + public function test_it_logs_in_a_tenant_bound_user_with_scan_permission(): void { $role = Role::query()->create([ - 'codigo' => RoleCode::Scanner->value, - 'nombre' => 'Scanner', + 'codigo' => RoleCode::AdminApp->value, + 'nombre' => 'Operador', ]); + $permission = Permission::query()->create([ + 'codigo' => PermissionCode::ScanTickets->value, + 'nombre' => 'Escanear tickets', + ]); + $role->permissions()->attach($permission->codigo); $tenant = Tenant::query()->create([ 'codigo' => 'acme', 'nombre' => 'Acme', @@ -40,22 +47,28 @@ class ScannerLoginControllerTest extends TestCase $response ->assertOk() ->assertJsonPath('user.id', $user->id) - ->assertJsonPath('user.rol_codigo', RoleCode::Scanner->value) + ->assertJsonPath('user.rol_codigo', RoleCode::AdminApp->value) ->assertJsonPath('token_type', 'Bearer'); $this->assertSame(['scanner'], $user->tokens()->sole()->abilities); } - public function test_it_rejects_a_non_scanner_user(): void + public function test_it_rejects_a_user_without_scan_permission(): void { $role = Role::query()->create([ - 'codigo' => RoleCode::User->value, - 'nombre' => 'Usuario', + 'codigo' => RoleCode::Scanner->value, + 'nombre' => 'Scanner sin permiso', + ]); + $tenant = Tenant::query()->create([ + 'codigo' => 'acme', + 'nombre' => 'Acme', + 'dominio' => 'acme.test', ]); $user = User::factory()->create([ 'email' => 'customer@example.com', 'password' => Hash::make('secret123'), 'rol_codigo' => $role->codigo, + 'tenant_codigo' => $tenant->codigo, ]); $this->postJson('/api/v1/scanner/login', [ diff --git a/tests/Feature/Auth/ScannerMeControllerTest.php b/tests/Feature/Auth/ScannerMeControllerTest.php index 9c00725..830fb8d 100644 --- a/tests/Feature/Auth/ScannerMeControllerTest.php +++ b/tests/Feature/Auth/ScannerMeControllerTest.php @@ -3,7 +3,9 @@ namespace Tests\Feature\Auth; use App\Domains\Auth\Models\User; +use App\Domains\Authorization\Enums\PermissionCode; use App\Domains\Authorization\Enums\RoleCode; +use App\Domains\Authorization\Models\Permission; use App\Domains\Authorization\Models\Role; use App\Domains\Menu\Models\Menu; use App\Domains\Tenant\Models\Tenant; @@ -21,6 +23,11 @@ class ScannerMeControllerTest extends TestCase 'codigo' => RoleCode::Scanner->value, 'nombre' => 'Scanner', ]); + $permission = Permission::query()->create([ + 'codigo' => PermissionCode::ScanTickets->value, + 'nombre' => 'Escanear tickets', + ]); + $scannerRole->permissions()->attach($permission->codigo); $tenant = Tenant::query()->create([ 'codigo' => 'acme', 'nombre' => 'Acme', diff --git a/tests/Feature/Seeders/AuthorizationSeederTest.php b/tests/Feature/Seeders/AuthorizationSeederTest.php index 92834ae..0889820 100644 --- a/tests/Feature/Seeders/AuthorizationSeederTest.php +++ b/tests/Feature/Seeders/AuthorizationSeederTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Seeders; +use App\Domains\Authorization\Enums\PermissionCode; use App\Domains\Authorization\Enums\RoleCode; use App\Domains\Authorization\Models\Permission; use App\Domains\Authorization\Models\Role; @@ -21,6 +22,7 @@ class AuthorizationSeederTest extends TestCase [ RoleCode::Admin->value, RoleCode::AdminApp->value, + RoleCode::Scanner->value, RoleCode::User->value, ], Role::query()->orderBy('codigo')->pluck('codigo')->all() @@ -34,10 +36,15 @@ class AuthorizationSeederTest extends TestCase $admin = Role::query()->where('codigo', RoleCode::Admin->value)->firstOrFail(); $appAdmin = Role::query()->where('codigo', RoleCode::AdminApp->value)->firstOrFail(); + $scanner = Role::query()->where('codigo', RoleCode::Scanner->value)->firstOrFail(); $user = Role::query()->where('codigo', RoleCode::User->value)->firstOrFail(); $this->assertCount(22, $admin->permissions); $this->assertCount(0, $appAdmin->permissions); + $this->assertSame( + [PermissionCode::ScanTickets->value], + $scanner->permissions->pluck('codigo')->all() + ); $this->assertCount(0, $user->permissions); } @@ -46,8 +53,8 @@ class AuthorizationSeederTest extends TestCase $this->seed(AuthorizationSeeder::class); $this->seed(AuthorizationSeeder::class); - $this->assertCount(3, Role::query()->get()); + $this->assertCount(4, Role::query()->get()); $this->assertCount(22, Permission::query()->get()); - $this->assertDatabaseCount('roles_permisos', 22); + $this->assertDatabaseCount('roles_permisos', 23); } } diff --git a/tests/Feature/Ticket/ScannerTicketControllerTest.php b/tests/Feature/Ticket/ScannerTicketControllerTest.php index f842c49..595cfce 100644 --- a/tests/Feature/Ticket/ScannerTicketControllerTest.php +++ b/tests/Feature/Ticket/ScannerTicketControllerTest.php @@ -47,7 +47,7 @@ class ScannerTicketControllerTest extends TestCase $this->scanner->scanCategories()->attach($this->category); } - public function test_scanner_routes_require_authentication_and_scanner_role(): void + public function test_scanner_routes_require_authentication_and_scan_permission(): void { $this->getJson('/api/v1/scanner/tickets')->assertUnauthorized();