Limit-Status im Upload-Flow anzeigen (Warnbanner + Sperrzustände).
Upload-Fehlercodes auswerten und freundliche Dialoge zeigen.
This commit is contained in:
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\User;
|
||||
use App\Support\ApiError;
|
||||
use Closure;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ApiTokenAuth
|
||||
{
|
||||
@@ -14,19 +17,30 @@ class ApiTokenAuth
|
||||
{
|
||||
$header = $request->header('Authorization', '');
|
||||
if (! str_starts_with($header, 'Bearer ')) {
|
||||
return response()->json(['error' => ['code' => 'unauthorized']], 401);
|
||||
return $this->unauthorizedResponse('missing_bearer');
|
||||
}
|
||||
$token = substr($header, 7);
|
||||
$userId = Cache::get('api_token:'.$token);
|
||||
if (! $userId) {
|
||||
return response()->json(['error' => ['code' => 'unauthorized']], 401);
|
||||
return $this->unauthorizedResponse('token_unknown');
|
||||
}
|
||||
$user = User::find($userId);
|
||||
if (! $user) {
|
||||
return response()->json(['error' => ['code' => 'unauthorized']], 401);
|
||||
return $this->unauthorizedResponse('user_missing');
|
||||
}
|
||||
Auth::login($user); // for policies if needed
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
private function unauthorizedResponse(string $reason): JsonResponse
|
||||
{
|
||||
return ApiError::response(
|
||||
'unauthorized',
|
||||
'Unauthorized',
|
||||
'Authentication is required to access this resource.',
|
||||
Response::HTTP_UNAUTHORIZED,
|
||||
['reason' => $reason]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Support\ApiError;
|
||||
use Closure;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class TenantIsolation
|
||||
{
|
||||
@@ -15,15 +18,15 @@ class TenantIsolation
|
||||
{
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
|
||||
if (!$tenantId) {
|
||||
return response()->json(['error' => 'Tenant ID not found in token'], 401);
|
||||
if (! $tenantId) {
|
||||
return $this->missingTenantIdResponse();
|
||||
}
|
||||
|
||||
// Get the tenant from request (query param, route param, or header)
|
||||
$requestTenantId = $this->getTenantIdFromRequest($request);
|
||||
|
||||
if ($requestTenantId && $requestTenantId != $tenantId) {
|
||||
return response()->json(['error' => 'Tenant isolation violation'], 403);
|
||||
return $this->tenantIsolationViolationResponse((int) $tenantId, (int) $requestTenantId);
|
||||
}
|
||||
|
||||
// Set tenant context for query scoping
|
||||
@@ -32,7 +35,6 @@ class TenantIsolation
|
||||
$connection->statement('SET @tenant_id = ?', [$tenantId]);
|
||||
}
|
||||
|
||||
|
||||
// Add tenant context to request for easy access in controllers
|
||||
$request->attributes->set('current_tenant_id', $tenantId);
|
||||
|
||||
@@ -62,4 +64,28 @@ class TenantIsolation
|
||||
// 4. For tenant-specific resources, use token tenant_id
|
||||
return null;
|
||||
}
|
||||
|
||||
private function missingTenantIdResponse(): JsonResponse
|
||||
{
|
||||
return ApiError::response(
|
||||
'tenant_context_missing',
|
||||
'Tenant Context Missing',
|
||||
'Tenant ID not found in access token.',
|
||||
Response::HTTP_UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
private function tenantIsolationViolationResponse(int $tokenTenantId, int $requestTenantId): JsonResponse
|
||||
{
|
||||
return ApiError::response(
|
||||
'tenant_isolation_violation',
|
||||
'Tenant Isolation Violation',
|
||||
'The requested resource belongs to a different tenant.',
|
||||
Response::HTTP_FORBIDDEN,
|
||||
[
|
||||
'token_tenant_id' => $tokenTenantId,
|
||||
'request_tenant_id' => $requestTenantId,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,18 @@ namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantToken;
|
||||
use App\Support\ApiError;
|
||||
use Closure;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Auth\GenericUser;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class TenantTokenGuard
|
||||
{
|
||||
@@ -26,36 +29,76 @@ class TenantTokenGuard
|
||||
$token = $this->getTokenFromRequest($request);
|
||||
|
||||
if (! $token) {
|
||||
return response()->json(['error' => 'Token not provided'], 401);
|
||||
return $this->errorResponse(
|
||||
'token_missing',
|
||||
'Token Missing',
|
||||
'Authentication token not provided.',
|
||||
Response::HTTP_UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = $this->decodeToken($token);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['error' => 'Invalid token'], 401);
|
||||
return $this->errorResponse(
|
||||
'token_invalid',
|
||||
'Invalid Token',
|
||||
'Authentication token cannot be decoded.',
|
||||
Response::HTTP_UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->isTokenBlacklisted($decoded)) {
|
||||
return response()->json(['error' => 'Token has been revoked'], 401);
|
||||
return $this->errorResponse(
|
||||
'token_revoked',
|
||||
'Token Revoked',
|
||||
'The provided token is no longer valid.',
|
||||
Response::HTTP_UNAUTHORIZED,
|
||||
['jti' => $decoded['jti'] ?? null]
|
||||
);
|
||||
}
|
||||
|
||||
if (! empty($scopes) && ! $this->hasScopes($decoded, $scopes)) {
|
||||
return response()->json(['error' => 'Insufficient scopes'], 403);
|
||||
return $this->errorResponse(
|
||||
'token_scope_violation',
|
||||
'Insufficient Scopes',
|
||||
'The provided token does not include the required scopes.',
|
||||
Response::HTTP_FORBIDDEN,
|
||||
['required_scopes' => $scopes, 'token_scopes' => $decoded['scopes'] ?? []]
|
||||
);
|
||||
}
|
||||
|
||||
if (($decoded['exp'] ?? 0) < time()) {
|
||||
$this->blacklistToken($decoded);
|
||||
return response()->json(['error' => 'Token expired'], 401);
|
||||
|
||||
return $this->errorResponse(
|
||||
'token_expired',
|
||||
'Token Expired',
|
||||
'Authentication token has expired.',
|
||||
Response::HTTP_UNAUTHORIZED,
|
||||
['expired_at' => $decoded['exp'] ?? null]
|
||||
);
|
||||
}
|
||||
|
||||
$tenantId = $decoded['tenant_id'] ?? $decoded['sub'] ?? null;
|
||||
if (! $tenantId) {
|
||||
return response()->json(['error' => 'Invalid token payload'], 401);
|
||||
return $this->errorResponse(
|
||||
'token_payload_invalid',
|
||||
'Invalid Token Payload',
|
||||
'Authentication token does not include tenant context.',
|
||||
Response::HTTP_UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
$tenant = Tenant::query()->find($tenantId);
|
||||
if (! $tenant) {
|
||||
return response()->json(['error' => 'Tenant not found'], 404);
|
||||
return $this->errorResponse(
|
||||
'tenant_not_found',
|
||||
'Tenant Not Found',
|
||||
'The tenant belonging to the token could not be located.',
|
||||
Response::HTTP_NOT_FOUND,
|
||||
['tenant_id' => $tenantId]
|
||||
);
|
||||
}
|
||||
|
||||
$scopesFromToken = $this->normaliseScopes($decoded['scopes'] ?? []);
|
||||
@@ -127,6 +170,7 @@ class TenantTokenGuard
|
||||
}
|
||||
|
||||
$decodedHeader = json_decode(base64_decode($segments[0]), true);
|
||||
|
||||
return is_array($decodedHeader) ? ($decodedHeader['kid'] ?? null) : null;
|
||||
}
|
||||
|
||||
@@ -170,12 +214,14 @@ class TenantTokenGuard
|
||||
|
||||
if ($tokenRecord->revoked_at) {
|
||||
Cache::put($cacheKey, true, $this->cacheTtlFromDecoded($decoded));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($tokenRecord->expires_at && $tokenRecord->expires_at->isPast()) {
|
||||
$tokenRecord->update(['revoked_at' => now()]);
|
||||
Cache::put($cacheKey, true, $this->cacheTtlFromDecoded($decoded));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -187,7 +233,7 @@ class TenantTokenGuard
|
||||
*/
|
||||
private function blacklistToken(array $decoded): void
|
||||
{
|
||||
$jti = $decoded['jti'] ?? md5(($decoded['sub'] ?? '') . ($decoded['iat'] ?? ''));
|
||||
$jti = $decoded['jti'] ?? md5(($decoded['sub'] ?? '').($decoded['iat'] ?? ''));
|
||||
$cacheKey = "blacklisted_token:{$jti}";
|
||||
|
||||
Cache::put($cacheKey, true, $this->cacheTtlFromDecoded($decoded));
|
||||
@@ -201,6 +247,7 @@ class TenantTokenGuard
|
||||
'revoked_at' => now(),
|
||||
'expires_at' => $record->expires_at ?? now(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -254,5 +301,9 @@ class TenantTokenGuard
|
||||
|
||||
return $ttl;
|
||||
}
|
||||
}
|
||||
|
||||
private function errorResponse(string $code, string $title, string $message, int $status, array $meta = []): JsonResponse
|
||||
{
|
||||
return ApiError::response($code, $title, $message, $status, $meta);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user