feat(admin): add admin scope management for users and events
- Introduced AdminScope enum for tenant and event scopes. - Updated User model to include admin_scope and event_id attributes. - Enhanced AdminAppAccessService to validate user scopes. - Modified AdminAppMeResource and UserResource to include event data. - Implemented middleware to ensure valid tenant access. - Created migration to add admin_scope and event_id to users table. - Added tests for event admin functionality and scope validation.
This commit is contained in:
9
app/Domains/Core/Auth/Enums/AdminScope.php
Normal file
9
app/Domains/Core/Auth/Enums/AdminScope.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Auth\Enums;
|
||||||
|
|
||||||
|
enum AdminScope: string
|
||||||
|
{
|
||||||
|
case Tenant = 'tenant';
|
||||||
|
case Event = 'event';
|
||||||
|
}
|
||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Domains\Core\Auth\Models;
|
namespace App\Domains\Core\Auth\Models;
|
||||||
|
|
||||||
|
use App\Domains\Commerce\Catalog\Models\Category;
|
||||||
|
use App\Domains\Core\Auth\Enums\AdminScope;
|
||||||
use App\Domains\Core\Authorization\Enums\RoleCode;
|
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||||
use App\Domains\Core\Authorization\Models\Role;
|
use App\Domains\Core\Authorization\Models\Role;
|
||||||
use App\Domains\Commerce\Catalog\Models\Category;
|
|
||||||
use App\Domains\Ticketing\Event\Models\EventDateChangeView;
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use App\Domains\Ticketing\Event\Models\EventDateChangeView;
|
||||||
use App\Domains\Ticketing\Ticket\Models\ScanAttempt;
|
use App\Domains\Ticketing\Ticket\Models\ScanAttempt;
|
||||||
use Database\Factories\UserFactory;
|
use Database\Factories\UserFactory;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
@@ -20,7 +22,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
|||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
#[Fillable(['nombre_apellido', 'email', 'password', 'dni', 'telefono', 'google_id', 'rol_codigo', 'tenant_codigo'])]
|
#[Fillable(['nombre_apellido', 'email', 'password', 'dni', 'telefono', 'google_id', 'rol_codigo', 'tenant_codigo', 'admin_scope', 'event_id'])]
|
||||||
#[Hidden(['password', 'remember_token', 'active_email', 'active_google_id'])]
|
#[Hidden(['password', 'remember_token', 'active_email', 'active_google_id'])]
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
@@ -29,6 +31,7 @@ class User extends Authenticatable
|
|||||||
|
|
||||||
protected $attributes = [
|
protected $attributes = [
|
||||||
'rol_codigo' => RoleCode::User->value,
|
'rol_codigo' => RoleCode::User->value,
|
||||||
|
'admin_scope' => AdminScope::Tenant->value,
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function newFactory(): UserFactory
|
protected static function newFactory(): UserFactory
|
||||||
@@ -86,6 +89,19 @@ class User extends Authenticatable
|
|||||||
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Event, $this> */
|
||||||
|
public function event(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTenantAdministrator(): bool
|
||||||
|
{
|
||||||
|
return $this->rol_codigo === RoleCode::AdminApp->value
|
||||||
|
&& $this->admin_scope === AdminScope::Tenant->value
|
||||||
|
&& $this->event_id === null;
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsToMany<Category, $this> */
|
/** @return BelongsToMany<Category, $this> */
|
||||||
public function scanCategories(): BelongsToMany
|
public function scanCategories(): BelongsToMany
|
||||||
{
|
{
|
||||||
@@ -103,6 +119,7 @@ class User extends Authenticatable
|
|||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'event_id' => 'integer',
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
'failed_login_attempts' => 'integer',
|
'failed_login_attempts' => 'integer',
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ class AdminAppMeResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'user' => UserResource::make($this->resource),
|
'user' => UserResource::make($this->resource),
|
||||||
'tenant' => TenantResource::make($this->tenant),
|
'tenant' => TenantResource::make($this->tenant),
|
||||||
|
'event' => $this->whenLoaded('event', fn () => $this->event === null ? null : [
|
||||||
|
'id' => $this->event->id,
|
||||||
|
'title' => $this->event->title,
|
||||||
|
'tenant_code' => $this->event->tenant_code,
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Domains\Core\Auth\Resources;
|
namespace App\Domains\Core\Auth\Resources;
|
||||||
|
|
||||||
use App\Domains\Core\Auth\Models\User;
|
use App\Domains\Core\Auth\Models\User;
|
||||||
|
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ class UserResource extends JsonResource
|
|||||||
'telefono' => $this->telefono,
|
'telefono' => $this->telefono,
|
||||||
'rol_codigo' => $this->rol_codigo,
|
'rol_codigo' => $this->rol_codigo,
|
||||||
'tenant_codigo' => $this->tenant_codigo,
|
'tenant_codigo' => $this->tenant_codigo,
|
||||||
|
'admin_scope' => $this->when($this->rol_codigo === RoleCode::AdminApp->value, $this->admin_scope),
|
||||||
|
'event_id' => $this->when($this->rol_codigo === RoleCode::AdminApp->value, $this->event_id),
|
||||||
'categories' => $this->whenLoaded('scanCategories', fn () => $this->scanCategories
|
'categories' => $this->whenLoaded('scanCategories', fn () => $this->scanCategories
|
||||||
->map(fn ($category) => [
|
->map(fn ($category) => [
|
||||||
'id' => $category->id,
|
'id' => $category->id,
|
||||||
|
|||||||
27
app/Domains/Core/Auth/Services/AdminAppAccessService.php
Normal file
27
app/Domains/Core/Auth/Services/AdminAppAccessService.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Auth\Services;
|
||||||
|
|
||||||
|
use App\Domains\Core\Auth\Enums\AdminScope;
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
|
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||||
|
|
||||||
|
class AdminAppAccessService
|
||||||
|
{
|
||||||
|
public function hasValidScope(User $user): bool
|
||||||
|
{
|
||||||
|
if ($user->rol_codigo !== RoleCode::AdminApp->value
|
||||||
|
|| ! $user->tenant_codigo
|
||||||
|
|| ! $user->tenant()->exists()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->isTenantAdministrator()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user->admin_scope === AdminScope::Event->value
|
||||||
|
&& $user->event_id !== null
|
||||||
|
&& $user->event()->where('tenant_code', $user->tenant_codigo)->exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,11 @@ class AdminAppContextService
|
|||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$user->setRelation('tenant', $tenant);
|
$user->setRelation('tenant', $tenant);
|
||||||
|
$user->load('event');
|
||||||
|
|
||||||
|
if (! $user->isTenantAdministrator()) {
|
||||||
|
$tenant->setRelation('menues', $tenant->menues->where('code', 'main.adminapp')->values());
|
||||||
|
}
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class PasswordLoginService
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ResetPasswordAttemptService $resetPasswordAttemptService,
|
private readonly ResetPasswordAttemptService $resetPasswordAttemptService,
|
||||||
|
private readonly AdminAppAccessService $adminAppAccessService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,6 +197,15 @@ class PasswordLoginService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($requiredRole === RoleCode::AdminApp && ! $this->adminAppAccessService->hasValidScope($user)) {
|
||||||
|
$this->recordAttempt(
|
||||||
|
$user, $normalizedEmail, $attemptTenantCode,
|
||||||
|
LoginAttempt::OUTCOME_INVALID_CREDENTIALS, $ipAddress, $userAgent,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ['outcome' => LoginAttempt::OUTCOME_INVALID_CREDENTIALS, 'user' => $user, 'locked_until' => null];
|
||||||
|
}
|
||||||
|
|
||||||
$user->forceFill([
|
$user->forceFill([
|
||||||
'failed_login_attempts' => 0,
|
'failed_login_attempts' => 0,
|
||||||
'last_failed_login_at' => null,
|
'last_failed_login_at' => null,
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ Route::prefix('v1/adminapp')->group(function (): void {
|
|||||||
Route::post('password/reset', ResetPasswordController::class)
|
Route::post('password/reset', ResetPasswordController::class)
|
||||||
->defaults('reset_role', 'adminapp')
|
->defaults('reset_role', 'adminapp')
|
||||||
->middleware('throttle:5,1');
|
->middleware('throttle:5,1');
|
||||||
Route::middleware(['auth:sanctum', 'adminapp.tenant'])
|
Route::middleware(['auth:sanctum', 'adminapp.tenant:context'])
|
||||||
->get('me', AdminAppMeController::class);
|
->get('me', AdminAppMeController::class);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Domains\Core\Auth\Services\AdminAppAccessService;
|
||||||
use App\Domains\Core\Authorization\Enums\RoleCode;
|
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
@@ -10,10 +11,12 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
|
|
||||||
class EnsureAdminAppTenant
|
class EnsureAdminAppTenant
|
||||||
{
|
{
|
||||||
|
public function __construct(private readonly AdminAppAccessService $accessService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the authenticated user is an AdminApp user bound to a tenant.
|
* Ensure the authenticated user is an AdminApp user bound to a tenant.
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next, string $access = 'tenant'): Response
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
@@ -21,6 +24,9 @@ class EnsureAdminAppTenant
|
|||||||
! $user
|
! $user
|
||||||
|| $user->rol_codigo !== RoleCode::AdminApp->value
|
|| $user->rol_codigo !== RoleCode::AdminApp->value
|
||||||
|| ! $user->tenant_codigo
|
|| ! $user->tenant_codigo
|
||||||
|
|| ! $this->accessService->hasValidScope($user)
|
||||||
|
// Tenant operations remain unavailable until they implement event authorization.
|
||||||
|
|| ($access !== 'context' && ! $user->isTenantAdministrator())
|
||||||
) {
|
) {
|
||||||
throw new AuthorizationException;
|
throw new AuthorizationException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table): void {
|
||||||
|
// Existing administrators keep their tenant-wide access.
|
||||||
|
$table->string('admin_scope', 20)->default('tenant');
|
||||||
|
$table->foreignId('event_id')->nullable()->constrained('events')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('event_id');
|
||||||
|
$table->dropColumn('admin_scope');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
203
tests/Feature/Auth/AdminAppEventScopeTest.php
Normal file
203
tests/Feature/Auth/AdminAppEventScopeTest.php
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Auth;
|
||||||
|
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Testing\TestResponse;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AdminAppEventScopeTest extends TestCase
|
||||||
|
{
|
||||||
|
private User $user;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Focused pre-migration schema: do not replay unrelated provisioning migrations.
|
||||||
|
// Tests\TestCase and tests/bootstrap.php enforce SQLite :memory:.
|
||||||
|
Schema::create('tenants', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('codigo')->unique();
|
||||||
|
$table->string('nombre');
|
||||||
|
$table->string('dominio');
|
||||||
|
$table->string('search_product_layout')->default('column_with_image');
|
||||||
|
$table->string('search_group_layout')->default('paginated');
|
||||||
|
});
|
||||||
|
Schema::create('events', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->string('title');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
Schema::create('users', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('nombre_apellido');
|
||||||
|
$table->string('email');
|
||||||
|
$table->string('active_email')->virtualAs('CASE WHEN deleted_at IS NULL THEN lower(email) ELSE NULL END');
|
||||||
|
$table->string('password');
|
||||||
|
$table->string('rol_codigo')->default('user');
|
||||||
|
$table->string('tenant_codigo')->nullable();
|
||||||
|
$table->softDeletes();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->unique(['active_email', 'rol_codigo']);
|
||||||
|
});
|
||||||
|
Schema::create('menues', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code')->unique();
|
||||||
|
$table->string('label');
|
||||||
|
$table->string('route');
|
||||||
|
$table->string('parent_menu_code')->nullable();
|
||||||
|
$table->string('content_type')->default('dynamic');
|
||||||
|
});
|
||||||
|
Schema::create('roles_menues', function (Blueprint $table): void {
|
||||||
|
$table->string('rol_codigo');
|
||||||
|
$table->string('menu_codigo');
|
||||||
|
});
|
||||||
|
Schema::create('tenants_menues', function (Blueprint $table): void {
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->string('menu_code');
|
||||||
|
$table->json('static_content')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
foreach ([
|
||||||
|
'2026_06_18_130006_create_personal_access_tokens_table.php',
|
||||||
|
'2026_07_28_000000_create_roles_and_permissions_tables.php',
|
||||||
|
'2026_07_29_000000_add_login_security_fields_to_users_table.php',
|
||||||
|
'2026_07_29_000100_create_login_attempts_table.php',
|
||||||
|
] as $file) {
|
||||||
|
(require database_path('migrations/'.$file))->up();
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('roles')->insert(['codigo' => 'adminapp', 'nombre' => 'AdminApp']);
|
||||||
|
DB::table('tenants')->insert([
|
||||||
|
['codigo' => 'onticket', 'nombre' => 'OnTicket', 'dominio' => 'onticket.test'],
|
||||||
|
['codigo' => 'other', 'nombre' => 'Other', 'dominio' => 'other.test'],
|
||||||
|
]);
|
||||||
|
DB::table('events')->insert([
|
||||||
|
['id' => 1, 'tenant_code' => 'onticket', 'title' => 'Evento A'],
|
||||||
|
['id' => 2, 'tenant_code' => 'onticket', 'title' => 'Evento B'],
|
||||||
|
['id' => 3, 'tenant_code' => 'other', 'title' => 'Evento ajeno'],
|
||||||
|
]);
|
||||||
|
// An existing administrator must remain general after applying the new migration.
|
||||||
|
DB::table('users')->insert([
|
||||||
|
'id' => 1, 'nombre_apellido' => 'Admin', 'email' => 'admin@example.test',
|
||||||
|
'password' => Hash::make('secret123'), 'rol_codigo' => 'adminapp', 'tenant_codigo' => 'onticket',
|
||||||
|
]);
|
||||||
|
$this->scopeMigration()->up();
|
||||||
|
$this->user = User::query()->findOrFail(1);
|
||||||
|
DB::table('menues')->insert([
|
||||||
|
['code' => 'main.adminapp', 'label' => 'Administración', 'route' => '/', 'parent_menu_code' => null],
|
||||||
|
['code' => 'onticket.adminapp.ventas', 'label' => 'Ventas', 'route' => '/admin/ventas', 'parent_menu_code' => 'main.adminapp'],
|
||||||
|
]);
|
||||||
|
foreach (['main.adminapp', 'onticket.adminapp.ventas'] as $code) {
|
||||||
|
DB::table('roles_menues')->insert(['rol_codigo' => 'adminapp', 'menu_codigo' => $code]);
|
||||||
|
DB::table('tenants_menues')->insert(['tenant_code' => 'onticket', 'menu_code' => $code]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function scopeMigration(): Migration
|
||||||
|
{
|
||||||
|
return require database_path('migrations/2026_09_30_000000_add_admin_scope_to_users.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function login(array $extra = []): TestResponse
|
||||||
|
{
|
||||||
|
return $this->postJson('/api/v1/adminapp/login', [
|
||||||
|
'email' => ' ADMIN@EXAMPLE.TEST ', 'password' => 'secret123', ...$extra,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_existing_admin_keeps_general_access_after_migration(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->user->isTenantAdministrator());
|
||||||
|
$token = $this->login()->assertOk()->assertJsonPath('user.admin_scope', 'tenant')
|
||||||
|
->assertJsonPath('user.event_id', null)->json('token');
|
||||||
|
$this->withToken($token)->getJson('/api/v1/adminapp/me')->assertOk()
|
||||||
|
->assertJsonPath('data.event', null)->assertJsonCount(1, 'data.tenant.menues.0.submenues');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_event_admin_logs_in_and_restores_only_its_assigned_context(): void
|
||||||
|
{
|
||||||
|
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
|
||||||
|
$this->assertSame(1, $this->user->event->id);
|
||||||
|
// Scope cannot be chosen by the caller during login.
|
||||||
|
$token = $this->login(['admin_scope' => 'tenant', 'event_id' => 2])->assertOk()
|
||||||
|
->assertJsonPath('user.admin_scope', 'event')->assertJsonPath('user.event_id', 1)->json('token');
|
||||||
|
$this->withToken($token)->getJson('/api/v1/adminapp/me')->assertOk()
|
||||||
|
->assertJsonPath('data.event.id', 1)->assertJsonPath('data.event.title', 'Evento A')
|
||||||
|
->assertJsonPath('data.tenant.codigo', 'onticket')
|
||||||
|
->assertJsonCount(0, 'data.tenant.menues.0.submenues');
|
||||||
|
$this->assertSame(['adminapp'], $this->user->tokens()->sole()->abilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invalid_scopes_never_issue_tokens_or_increment_password_failure_counts(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
['admin_scope' => 'event', 'event_id' => null],
|
||||||
|
['admin_scope' => 'event', 'event_id' => 3],
|
||||||
|
['admin_scope' => 'tenant', 'event_id' => 1],
|
||||||
|
['admin_scope' => 'unknown', 'event_id' => null],
|
||||||
|
['admin_scope' => 'event', 'event_id' => 1, 'tenant_codigo' => null],
|
||||||
|
] as $attributes) {
|
||||||
|
$this->user->update($attributes);
|
||||||
|
$this->login()->assertUnprocessable()->assertJsonValidationErrors('email');
|
||||||
|
}
|
||||||
|
$this->assertDatabaseCount('personal_access_tokens', 0);
|
||||||
|
$this->assertSame(0, $this->user->refresh()->failed_login_attempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_event_admin_cannot_access_tenant_operations_but_can_logout(): void
|
||||||
|
{
|
||||||
|
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
|
||||||
|
$token = $this->login()->assertOk()->json('token');
|
||||||
|
foreach (['tenant/sales', 'tenant/tickets', 'tenant/event', 'tenant/administrators', 'forms/event'] as $path) {
|
||||||
|
$this->withToken($token)->getJson('/api/v1/adminapp/'.$path)->assertForbidden();
|
||||||
|
}
|
||||||
|
$this->withToken($token)->postJson('/api/v1/adminapp/tenant/administrators', [])->assertForbidden();
|
||||||
|
$this->withToken($token)->postJson('/api/logout')->assertOk();
|
||||||
|
$this->assertDatabaseCount('personal_access_tokens', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_deleting_the_event_invalidates_an_existing_token_without_promoting_the_user(): void
|
||||||
|
{
|
||||||
|
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
|
||||||
|
$token = $this->login()->assertOk()->json('token');
|
||||||
|
DB::table('events')->where('id', 1)->delete();
|
||||||
|
$this->assertNull($this->user->refresh()->event_id);
|
||||||
|
$this->assertSame('event', $this->user->admin_scope);
|
||||||
|
$this->withToken($token)->getJson('/api/v1/adminapp/me')->assertForbidden();
|
||||||
|
$this->withToken($token)->getJson('/api/v1/adminapp/tenant/administrators')->assertForbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_event_reassignment_and_tenant_changes_apply_to_existing_tokens(): void
|
||||||
|
{
|
||||||
|
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
|
||||||
|
$token = $this->login()->assertOk()->json('token');
|
||||||
|
$this->user->update(['event_id' => 2]);
|
||||||
|
$this->withToken($token)->getJson('/api/v1/adminapp/me')->assertOk()->assertJsonPath('data.event.id', 2);
|
||||||
|
DB::table('events')->where('id', 2)->update(['tenant_code' => 'other']);
|
||||||
|
$this->withToken($token)->getJson('/api/v1/adminapp/me')->assertForbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_wrong_password_still_counts_as_a_failed_attempt(): void
|
||||||
|
{
|
||||||
|
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
|
||||||
|
$this->login(['password' => 'wrong'])->assertUnprocessable()->assertJsonValidationErrors('email');
|
||||||
|
$this->assertSame(1, $this->user->refresh()->failed_login_attempts);
|
||||||
|
$this->assertDatabaseCount('personal_access_tokens', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scope_migration_can_be_rolled_back_without_removing_users(): void
|
||||||
|
{
|
||||||
|
$this->scopeMigration()->down();
|
||||||
|
$this->assertFalse(Schema::hasColumn('users', 'admin_scope'));
|
||||||
|
$this->assertFalse(Schema::hasColumn('users', 'event_id'));
|
||||||
|
$this->assertDatabaseHas('users', ['id' => 1, 'email' => 'admin@example.test']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user