Limit-Status im Upload-Flow anzeigen (Warnbanner + Sperrzustände).

Upload-Fehlercodes auswerten und freundliche Dialoge zeigen.
This commit is contained in:
Codex Agent
2025-11-01 19:50:17 +01:00
parent 2c14493604
commit 79b209de9a
55 changed files with 3348 additions and 462 deletions

View File

@@ -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);
}
}