photo-upload und ansicht im admin gefixt

This commit is contained in:
Codex Agent
2025-12-16 16:19:23 +01:00
parent 9e4e9a0d87
commit 0aae494945
4 changed files with 174 additions and 6 deletions

View File

@@ -2789,6 +2789,7 @@ class EventPublicController extends BaseController
$photoId = DB::table('photos')->insertGetId([
'event_id' => $eventId,
'tenant_id' => $tenantModel->id,
'task_id' => $validated['task_id'] ?? null,
'guest_name' => $validated['guest_name'] ?? $deviceId,
'file_path' => $url,

View File

@@ -23,6 +23,7 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\Response;
class PhotoController extends Controller
@@ -136,6 +137,117 @@ class PhotoController extends Controller
]);
}
public function asset(Request $request, Event $event, Photo $photo, string $variant): StreamedResponse|JsonResponse
{
if ($photo->event_id !== $event->id) {
return ApiError::response(
'photo_not_found',
'Foto nicht gefunden',
'Das Foto gehört nicht zu diesem Event.',
Response::HTTP_NOT_FOUND,
['photo_id' => $photo->id]
);
}
$preferOriginals = (bool) ($event->settings['watermark_serve_originals'] ?? false);
[$disk, $path, $mime] = $this->resolvePhotoVariant($photo, $variant, $preferOriginals);
if (! $path) {
return ApiError::response(
'photo_unavailable',
'Photo Unavailable',
'The requested photo could not be loaded.',
Response::HTTP_NOT_FOUND,
[
'photo_id' => $photo->id,
'event_id' => $event->id,
]
);
}
if (Str::startsWith($path, ['http://', 'https://'])) {
return redirect()->away($path);
}
try {
$storage = Storage::disk($disk);
} catch (\Throwable $e) {
Log::warning('Tenant photo asset disk unavailable', [
'event_id' => $event->id,
'photo_id' => $photo->id,
'disk' => $disk,
'error' => $e->getMessage(),
]);
return ApiError::response(
'photo_unavailable',
'Photo Unavailable',
'The requested photo could not be loaded.',
Response::HTTP_NOT_FOUND,
[
'photo_id' => $photo->id,
'event_id' => $event->id,
]
);
}
foreach ($this->storagePathCandidates($path) as $candidate) {
if (! $storage->exists($candidate)) {
continue;
}
$stream = $storage->readStream($candidate);
if (! $stream) {
continue;
}
$size = null;
try {
$size = $storage->size($candidate);
} catch (\Throwable $e) {
$size = null;
}
if (! $mime) {
try {
$mime = $storage->mimeType($candidate);
} catch (\Throwable $e) {
$mime = null;
}
}
$extension = pathinfo($candidate, PATHINFO_EXTENSION) ?: ($photo->mime_type ? explode('/', $photo->mime_type)[1] ?? 'jpg' : 'jpg');
$suffix = $variant === 'thumbnail' ? '-thumb' : '';
$filename = sprintf('fotospiel-event-%s-photo-%s%s.%s', $event->id, $photo->id, $suffix, $extension ?: 'jpg');
$headers = [
'Content-Type' => $mime ?? 'image/jpeg',
'Cache-Control' => 'private, max-age=1800',
'Content-Disposition' => 'inline; filename="'.$filename.'"',
];
if ($size) {
$headers['Content-Length'] = $size;
}
return response()->stream(function () use ($stream) {
fpassthru($stream);
fclose($stream);
}, 200, $headers);
}
return ApiError::response(
'photo_unavailable',
'Photo Unavailable',
'The requested photo could not be loaded.',
Response::HTTP_NOT_FOUND,
[
'photo_id' => $photo->id,
'event_id' => $event->id,
]
);
}
/**
* Store a newly uploaded photo.
*/
@@ -856,4 +968,57 @@ class PhotoController extends Controller
'status' => 'pending',
], 201);
}
private function resolvePhotoVariant(Photo $record, string $variant, bool $preferOriginals = false): array
{
if ($variant === 'thumbnail') {
$asset = EventMediaAsset::where('photo_id', $record->id)->where('variant', 'thumbnail')->first();
$watermarked = $preferOriginals
? null
: EventMediaAsset::where('photo_id', $record->id)->where('variant', 'watermarked_thumbnail')->first();
$disk = $asset?->disk ?? $record->mediaAsset?->disk;
$path = $watermarked?->path ?? $asset?->path ?? ($record->thumbnail_path ?: $record->file_path);
$mime = $watermarked?->mime_type ?? $asset?->mime_type ?? 'image/jpeg';
} else {
$watermarked = $preferOriginals
? null
: EventMediaAsset::where('photo_id', $record->id)->where('variant', 'watermarked')->first();
$asset = $record->mediaAsset ?? $watermarked ?? EventMediaAsset::where('photo_id', $record->id)->where('variant', 'original')->first();
$disk = $asset?->disk ?? $record->mediaAsset?->disk;
$path = $watermarked?->path ?? $asset?->path ?? ($record->file_path ?? null);
$mime = $watermarked?->mime_type ?? $asset?->mime_type ?? ($record->mime_type ?? 'image/jpeg');
}
return [
$disk ?: config('filesystems.default', 'public'),
$path,
$mime,
];
}
private function storagePathCandidates(string $path): array
{
$normalized = str_replace('\\', '/', $path);
$candidates = [$normalized];
$trimmed = ltrim($normalized, '/');
if ($trimmed !== $normalized) {
$candidates[] = $trimmed;
}
if (str_starts_with($trimmed, 'storage/')) {
$candidates[] = substr($trimmed, strlen('storage/'));
}
if (str_starts_with($trimmed, 'public/')) {
$candidates[] = substr($trimmed, strlen('public/'));
}
$needle = '/storage/app/public/';
if (str_contains($normalized, $needle)) {
$candidates[] = substr($normalized, strpos($normalized, $needle) + strlen($needle));
}
return array_values(array_unique(array_filter($candidates)));
}
}

View File

@@ -54,15 +54,11 @@ class PhotoResource extends JsonResource
return null;
}
$route = $variant === 'thumbnail'
? 'api.v1.gallery.photos.asset'
: 'api.v1.gallery.photos.asset';
return \URL::temporarySignedRoute(
$route,
'api.v1.tenant.events.photos.asset',
now()->addMinutes(30),
[
'token' => $this->event->slug, // tenant/admin views are trusted; token not used server-side for signed validation
'event' => $this->event->slug,
'photo' => $this->id,
'variant' => $variant === 'thumbnail' ? 'thumbnail' : 'full',
]

View File

@@ -127,6 +127,12 @@ Route::prefix('v1')->name('api.v1.')->group(function () {
Route::post('/photobooth/sparkbooth/upload', [SparkboothUploadController::class, 'store'])
->name('photobooth.sparkbooth.upload');
Route::get('/tenant/events/{event:slug}/photos/{photo}/{variant}/asset', [PhotoController::class, 'asset'])
->whereNumber('photo')
->where('variant', 'thumbnail|full')
->middleware('signed')
->name('tenant.events.photos.asset');
});
Route::middleware(['auth:sanctum', 'tenant.collaborator', 'tenant.isolation', 'throttle:tenant-api'])->prefix('tenant')->group(function () {