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;
|
||||
|
||||
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',
|
||||
|
||||
@@ -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,
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
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();
|
||||
|
||||
$user->setRelation('tenant', $tenant);
|
||||
$user->load('event');
|
||||
|
||||
if (! $user->isTenantAdministrator()) {
|
||||
$tenant->setRelation('menues', $tenant->menues->where('code', 'main.adminapp')->values());
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Domains\Core\Auth\Services\AdminAppAccessService;
|
||||
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||
use Closure;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
@@ -10,10 +11,12 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureAdminAppTenant
|
||||
{
|
||||
public function __construct(private readonly AdminAppAccessService $accessService) {}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
@@ -21,6 +24,9 @@ class EnsureAdminAppTenant
|
||||
! $user
|
||||
|| $user->rol_codigo !== RoleCode::AdminApp->value
|
||||
|| ! $user->tenant_codigo
|
||||
|| ! $this->accessService->hasValidScope($user)
|
||||
// Tenant operations remain unavailable until they implement event authorization.
|
||||
|| ($access !== 'context' && ! $user->isTenantAdministrator())
|
||||
) {
|
||||
throw new AuthorizationException;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user