148 lines
4.0 KiB
PHP
148 lines
4.0 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Support\ApiError;
|
|
use Closure;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Laravel\Sanctum\PersonalAccessToken;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class EnsureTenantAdminToken
|
|
{
|
|
/**
|
|
* Handle an incoming request.
|
|
*/
|
|
public function handle(Request $request, Closure $next): JsonResponse|Response
|
|
{
|
|
$user = $request->user();
|
|
|
|
if (! $user) {
|
|
return $this->unauthorizedResponse('Unauthenticated request.');
|
|
}
|
|
|
|
$accessToken = $user->currentAccessToken();
|
|
|
|
if (! $accessToken instanceof PersonalAccessToken) {
|
|
return $this->unauthorizedResponse('Missing personal access token context.');
|
|
}
|
|
|
|
if (! in_array($user->role, $this->allowedRoles(), true)) {
|
|
return $this->forbiddenResponse($this->forbiddenRoleMessage());
|
|
}
|
|
|
|
if (! $this->hasRequiredAbilities($accessToken, $user)) {
|
|
return $this->forbiddenResponse($this->abilityErrorMessage());
|
|
}
|
|
|
|
/** @var Tenant|null $tenant */
|
|
$tenant = $user->tenant;
|
|
|
|
if (! $tenant && $user->isSuperAdmin()) {
|
|
$requestedTenantId = $this->resolveRequestedTenantId($request);
|
|
|
|
if ($requestedTenantId !== null) {
|
|
$tenant = Tenant::query()->find($requestedTenantId);
|
|
}
|
|
}
|
|
|
|
if (! $tenant && ! $user->isSuperAdmin()) {
|
|
return $this->forbiddenResponse('Tenant context missing for user.');
|
|
}
|
|
|
|
if ($tenant) {
|
|
$request->attributes->set('tenant_id', $tenant->id);
|
|
$request->attributes->set('tenant', $tenant);
|
|
} elseif ($user->isSuperAdmin()) {
|
|
$requestedTenantId = $this->resolveRequestedTenantId($request);
|
|
if ($requestedTenantId !== null) {
|
|
$request->attributes->set('tenant_id', $requestedTenantId);
|
|
}
|
|
}
|
|
|
|
$request->attributes->set('sanctum_token_id', $accessToken->id);
|
|
|
|
Auth::shouldUse('sanctum');
|
|
|
|
return $next($request);
|
|
}
|
|
|
|
private function unauthorizedResponse(string $message): JsonResponse
|
|
{
|
|
return ApiError::response(
|
|
'unauthenticated',
|
|
'Unauthenticated',
|
|
$message,
|
|
Response::HTTP_UNAUTHORIZED
|
|
);
|
|
}
|
|
|
|
private function forbiddenResponse(string $message): JsonResponse
|
|
{
|
|
return ApiError::response(
|
|
'tenant_admin_only',
|
|
'Forbidden',
|
|
$message,
|
|
Response::HTTP_FORBIDDEN
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
protected function allowedRoles(): array
|
|
{
|
|
return ['tenant_admin', 'super_admin', 'superadmin', 'admin'];
|
|
}
|
|
|
|
protected function forbiddenRoleMessage(): string
|
|
{
|
|
return 'Only tenant administrators may access this resource.';
|
|
}
|
|
|
|
protected function requiredAbilitiesForUser(User $user): array
|
|
{
|
|
return ['tenant-admin'];
|
|
}
|
|
|
|
protected function abilityErrorMessage(): string
|
|
{
|
|
return 'Access token does not include the tenant-admin ability.';
|
|
}
|
|
|
|
protected function hasRequiredAbilities(PersonalAccessToken $accessToken, User $user): bool
|
|
{
|
|
foreach ($this->requiredAbilitiesForUser($user) as $ability) {
|
|
if (! $accessToken->can($ability)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function resolveRequestedTenantId(Request $request): ?int
|
|
{
|
|
$routeTenant = $request->route('tenant');
|
|
if (is_numeric($routeTenant)) {
|
|
return (int) $routeTenant;
|
|
}
|
|
|
|
$queryTenant = $request->query('tenant_id');
|
|
if (is_numeric($queryTenant)) {
|
|
return (int) $queryTenant;
|
|
}
|
|
|
|
$headerTenant = $request->header('X-Tenant-ID');
|
|
if (is_numeric($headerTenant)) {
|
|
return (int) $headerTenant;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|