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:
2026-09-30 15:02:10 -03:00
parent 6e1f0d731c
commit 44bf67bd12
11 changed files with 315 additions and 5 deletions

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Domains\Core\Auth\Enums;
enum AdminScope: string
{
case Tenant = 'tenant';
case Event = 'event';
}

View File

@@ -2,11 +2,13 @@
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\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\Ticketing\Event\Models\Event;
use App\Domains\Ticketing\Event\Models\EventDateChangeView;
use App\Domains\Ticketing\Ticket\Models\ScanAttempt;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
@@ -20,7 +22,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
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'])]
class User extends Authenticatable
{
@@ -29,6 +31,7 @@ class User extends Authenticatable
protected $attributes = [
'rol_codigo' => RoleCode::User->value,
'admin_scope' => AdminScope::Tenant->value,
];
protected static function newFactory(): UserFactory
@@ -86,6 +89,19 @@ class User extends Authenticatable
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> */
public function scanCategories(): BelongsToMany
{
@@ -103,6 +119,7 @@ class User extends Authenticatable
protected function casts(): array
{
return [
'event_id' => 'integer',
'email_verified_at' => 'datetime',
'password' => 'hashed',
'failed_login_attempts' => 'integer',

View File

@@ -20,6 +20,11 @@ class AdminAppMeResource extends JsonResource
return [
'user' => UserResource::make($this->resource),
'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,
]),
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Domains\Core\Auth\Resources;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -24,6 +25,8 @@ class UserResource extends JsonResource
'telefono' => $this->telefono,
'rol_codigo' => $this->rol_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
->map(fn ($category) => [
'id' => $category->id,

View 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();
}
}

View File

@@ -20,6 +20,11 @@ class AdminAppContextService
->firstOrFail();
$user->setRelation('tenant', $tenant);
$user->load('event');
if (! $user->isTenantAdministrator()) {
$tenant->setRelation('menues', $tenant->menues->where('code', 'main.adminapp')->values());
}
return $user;
}

View File

@@ -19,6 +19,7 @@ class PasswordLoginService
{
public function __construct(
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([
'failed_login_attempts' => 0,
'last_failed_login_at' => null,

View File

@@ -17,6 +17,6 @@ Route::prefix('v1/adminapp')->group(function (): void {
Route::post('password/reset', ResetPasswordController::class)
->defaults('reset_role', 'adminapp')
->middleware('throttle:5,1');
Route::middleware(['auth:sanctum', 'adminapp.tenant'])
Route::middleware(['auth:sanctum', 'adminapp.tenant:context'])
->get('me', AdminAppMeController::class);
});