Limit-Status im Upload-Flow anzeigen (Warnbanner + Sperrzustände).
Upload-Fehlercodes auswerten und freundliche Dialoge zeigen.
This commit is contained in:
@@ -150,12 +150,16 @@ class EventPublicController extends BaseController
|
||||
Response::HTTP_FORBIDDEN
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'event_not_public',
|
||||
'message' => 'This event is not publicly accessible.',
|
||||
],
|
||||
], Response::HTTP_FORBIDDEN);
|
||||
return ApiError::response(
|
||||
'event_not_public',
|
||||
'Event Not Public',
|
||||
'This event is not publicly accessible.',
|
||||
Response::HTTP_FORBIDDEN,
|
||||
[
|
||||
'token' => Str::limit($token, 12),
|
||||
'event_id' => $event->id ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
RateLimiter::clear($rateLimiterKey);
|
||||
@@ -199,12 +203,15 @@ class EventPublicController extends BaseController
|
||||
$event = Event::with(['tenant', 'eventPackage.package'])->find($eventRecord->id);
|
||||
|
||||
if (! $event) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'event_not_found',
|
||||
'message' => 'The event associated with this gallery could not be located.',
|
||||
],
|
||||
], Response::HTTP_NOT_FOUND);
|
||||
return ApiError::response(
|
||||
'event_not_found',
|
||||
'Event Not Found',
|
||||
'The event associated with this gallery could not be located.',
|
||||
Response::HTTP_NOT_FOUND,
|
||||
[
|
||||
'token' => Str::limit($token, 12),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$expiresAt = optional($event->eventPackage)->gallery_expires_at;
|
||||
@@ -222,13 +229,16 @@ class EventPublicController extends BaseController
|
||||
Response::HTTP_GONE
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'gallery_expired',
|
||||
'message' => 'The gallery is no longer available for this event.',
|
||||
return ApiError::response(
|
||||
'gallery_expired',
|
||||
'Gallery Expired',
|
||||
'The gallery is no longer available for this event.',
|
||||
Response::HTTP_GONE,
|
||||
[
|
||||
'event_id' => $event->id,
|
||||
'expired_at' => $expiresAt->toIso8601String(),
|
||||
],
|
||||
], Response::HTTP_GONE);
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->recordTokenEvent(
|
||||
@@ -271,12 +281,13 @@ class EventPublicController extends BaseController
|
||||
Response::HTTP_TOO_MANY_REQUESTS
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'token_rate_limited',
|
||||
'message' => 'Too many invalid join token attempts. Try again later.',
|
||||
],
|
||||
], Response::HTTP_TOO_MANY_REQUESTS);
|
||||
return ApiError::response(
|
||||
'token_rate_limited',
|
||||
'Too Many Attempts',
|
||||
'Too many invalid join token attempts. Try again later.',
|
||||
Response::HTTP_TOO_MANY_REQUESTS,
|
||||
array_merge($context, ['rate_limiter_key' => $rateLimiterKey])
|
||||
);
|
||||
}
|
||||
|
||||
RateLimiter::hit($rateLimiterKey, $failureDecay * 60);
|
||||
@@ -295,12 +306,13 @@ class EventPublicController extends BaseController
|
||||
$status
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => $code,
|
||||
'message' => $this->tokenErrorMessage($code),
|
||||
],
|
||||
], $status);
|
||||
return ApiError::response(
|
||||
$code,
|
||||
$this->tokenErrorTitle($code),
|
||||
$this->tokenErrorMessage($code),
|
||||
$status,
|
||||
$context
|
||||
);
|
||||
}
|
||||
|
||||
private function tokenErrorMessage(string $code): string
|
||||
@@ -313,6 +325,17 @@ class EventPublicController extends BaseController
|
||||
};
|
||||
}
|
||||
|
||||
private function tokenErrorTitle(string $code): string
|
||||
{
|
||||
return match ($code) {
|
||||
'invalid_token' => 'Invalid Join Token',
|
||||
'token_expired' => 'Join Token Expired',
|
||||
'token_revoked' => 'Join Token Revoked',
|
||||
'token_rate_limited' => 'Join Token Rate Limited',
|
||||
default => 'Access Denied',
|
||||
};
|
||||
}
|
||||
|
||||
private function recordTokenEvent(
|
||||
?EventJoinToken $joinToken,
|
||||
Request $request,
|
||||
@@ -347,12 +370,16 @@ class EventPublicController extends BaseController
|
||||
Response::HTTP_TOO_MANY_REQUESTS
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'access_rate_limited',
|
||||
'message' => 'Too many requests. Please slow down.',
|
||||
],
|
||||
], Response::HTTP_TOO_MANY_REQUESTS);
|
||||
return ApiError::response(
|
||||
'access_rate_limited',
|
||||
'Too Many Requests',
|
||||
'Too many requests. Please slow down.',
|
||||
Response::HTTP_TOO_MANY_REQUESTS,
|
||||
[
|
||||
'limit' => $limit,
|
||||
'decay_minutes' => $decay,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
RateLimiter::hit($key, $decay * 60);
|
||||
@@ -383,12 +410,16 @@ class EventPublicController extends BaseController
|
||||
Response::HTTP_TOO_MANY_REQUESTS
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'download_rate_limited',
|
||||
'message' => 'Download rate limit exceeded. Please wait a moment.',
|
||||
],
|
||||
], Response::HTTP_TOO_MANY_REQUESTS);
|
||||
return ApiError::response(
|
||||
'download_rate_limited',
|
||||
'Download Rate Limited',
|
||||
'Download rate limit exceeded. Please wait a moment.',
|
||||
Response::HTTP_TOO_MANY_REQUESTS,
|
||||
[
|
||||
'limit' => $limit,
|
||||
'decay_minutes' => $decay,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
RateLimiter::hit($key, $decay * 60);
|
||||
@@ -664,12 +695,16 @@ class EventPublicController extends BaseController
|
||||
->first();
|
||||
|
||||
if (! $record) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'photo_not_found',
|
||||
'message' => 'The requested photo is no longer available.',
|
||||
],
|
||||
], Response::HTTP_NOT_FOUND);
|
||||
return ApiError::response(
|
||||
'photo_not_found',
|
||||
'Photo Not Found',
|
||||
'The requested photo is no longer available.',
|
||||
Response::HTTP_NOT_FOUND,
|
||||
[
|
||||
'photo_id' => $photo,
|
||||
'event_id' => $event->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$variantPreference = $variant === 'thumbnail'
|
||||
@@ -697,12 +732,16 @@ class EventPublicController extends BaseController
|
||||
->first();
|
||||
|
||||
if (! $record) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'photo_not_found',
|
||||
'message' => 'The requested photo is no longer available.',
|
||||
],
|
||||
], Response::HTTP_NOT_FOUND);
|
||||
return ApiError::response(
|
||||
'photo_not_found',
|
||||
'Photo Not Found',
|
||||
'The requested photo is no longer available.',
|
||||
Response::HTTP_NOT_FOUND,
|
||||
[
|
||||
'photo_id' => $photo,
|
||||
'event_id' => $event->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $this->streamGalleryPhoto($event, $record, ['original'], 'attachment');
|
||||
@@ -763,6 +802,69 @@ class EventPublicController extends BaseController
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
public function package(Request $request, string $token)
|
||||
{
|
||||
$result = $this->resolvePublishedEvent($request, $token, ['id']);
|
||||
|
||||
if ($result instanceof JsonResponse) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
[$eventRecord, $joinToken] = $result;
|
||||
|
||||
$event = Event::with(['tenant', 'eventPackage.package', 'eventPackages.package'])
|
||||
->findOrFail($eventRecord->id);
|
||||
|
||||
if (! $event->tenant) {
|
||||
return ApiError::response(
|
||||
'event_not_found',
|
||||
'Event not accessible',
|
||||
'The selected event is no longer available.',
|
||||
Response::HTTP_NOT_FOUND,
|
||||
['scope' => 'photos', 'event_id' => $event->id]
|
||||
);
|
||||
}
|
||||
|
||||
$eventPackage = $this->packageLimitEvaluator->resolveEventPackageForPhotoUpload(
|
||||
$event->tenant,
|
||||
$event->id,
|
||||
$event
|
||||
);
|
||||
|
||||
if (! $eventPackage || ! $eventPackage->package) {
|
||||
return response()->json([
|
||||
'id' => null,
|
||||
'event_id' => $event->id,
|
||||
'package_id' => null,
|
||||
'package' => null,
|
||||
'used_photos' => (int) ($eventPackage?->used_photos ?? 0),
|
||||
'used_guests' => (int) ($eventPackage?->used_guests ?? 0),
|
||||
'expires_at' => $eventPackage?->gallery_expires_at?->toIso8601String(),
|
||||
'limits' => null,
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
$package = $eventPackage->package;
|
||||
$summary = $this->packageLimitEvaluator->summarizeEventPackage($eventPackage);
|
||||
|
||||
return response()->json([
|
||||
'id' => $eventPackage->id,
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $eventPackage->package_id,
|
||||
'package' => [
|
||||
'id' => $eventPackage->package_id,
|
||||
'name' => $package?->getNameForLocale(app()->getLocale()) ?? $package?->name,
|
||||
'max_photos' => $package?->max_photos,
|
||||
'max_guests' => $package?->max_guests,
|
||||
'gallery_days' => $package?->gallery_days,
|
||||
],
|
||||
'used_photos' => (int) $eventPackage->used_photos,
|
||||
'used_guests' => (int) $eventPackage->used_guests,
|
||||
'expires_at' => $eventPackage->gallery_expires_at?->toIso8601String(),
|
||||
'limits' => $summary,
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
|
||||
private function streamGalleryPhoto(Event $event, Photo $record, array $variantPreference, string $disposition)
|
||||
{
|
||||
foreach ($variantPreference as $variant) {
|
||||
@@ -852,12 +954,16 @@ class EventPublicController extends BaseController
|
||||
return redirect()->away($fallbackUrl);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'photo_unavailable',
|
||||
'message' => 'The requested photo could not be loaded.',
|
||||
],
|
||||
], Response::HTTP_NOT_FOUND);
|
||||
return ApiError::response(
|
||||
'photo_unavailable',
|
||||
'Photo Unavailable',
|
||||
'The requested photo could not be loaded.',
|
||||
Response::HTTP_NOT_FOUND,
|
||||
[
|
||||
'photo_id' => $record->id,
|
||||
'event_id' => $event->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function resolvePhotoVariant(Photo $record, string $variant): array
|
||||
@@ -1191,7 +1297,13 @@ class EventPublicController extends BaseController
|
||||
->where('events.status', 'published')
|
||||
->first();
|
||||
if (! $row) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found or event not public']], 404);
|
||||
return ApiError::response(
|
||||
'photo_not_found',
|
||||
'Photo Not Found',
|
||||
'Photo not found or event not public.',
|
||||
Response::HTTP_NOT_FOUND,
|
||||
['photo_id' => $id]
|
||||
);
|
||||
}
|
||||
$row->file_path = $this->toPublicUrl((string) ($row->file_path ?? ''));
|
||||
$row->thumbnail_path = $this->toPublicUrl((string) ($row->thumbnail_path ?? ''));
|
||||
@@ -1219,7 +1331,13 @@ class EventPublicController extends BaseController
|
||||
->where('events.status', 'published')
|
||||
->first(['photos.id', 'photos.event_id']);
|
||||
if (! $photo) {
|
||||
return response()->json(['error' => ['code' => 'not_found', 'message' => 'Photo not found or event not public']], 404);
|
||||
return ApiError::response(
|
||||
'photo_not_found',
|
||||
'Photo Not Found',
|
||||
'Photo not found or event not public.',
|
||||
Response::HTTP_NOT_FOUND,
|
||||
['photo_id' => $id]
|
||||
);
|
||||
}
|
||||
|
||||
// Idempotent like per device
|
||||
|
||||
Reference in New Issue
Block a user