resolveTenant($request); $exports = DataExport::query() ->with('event') ->where('tenant_id', $tenant->id) ->whereIn('scope', ['tenant', 'event']) ->latest() ->limit(10) ->get() ->map(fn (DataExport $export) => [ 'id' => $export->id, 'scope' => $export->scope?->value ?? $export->scope, 'status' => $export->status, 'include_media' => (bool) $export->include_media, 'size_bytes' => $export->size_bytes, 'created_at' => optional($export->created_at)->toIso8601String(), 'expires_at' => optional($export->expires_at)->toIso8601String(), 'download_url' => $export->isReady() && ! $export->hasExpired() ? route('api.v1.tenant.exports.download', $export) : null, 'error_message' => $export->error_message, 'event' => $export->event ? [ 'id' => $export->event->id, 'slug' => $export->event->slug, 'name' => $export->event->name, ] : null, ]); return response()->json([ 'data' => $exports, ]); } public function store(DataExportStoreRequest $request): JsonResponse { $tenant = $this->resolveTenant($request); $user = $request->user(); if (! $user) { return ApiError::response( 'export_user_missing', 'Export user missing', 'Unable to determine the requesting user.', Response::HTTP_UNAUTHORIZED ); } $payload = $request->validated(); $scope = $payload['scope']; $event = null; if ($scope === 'event') { $event = Event::query() ->where('tenant_id', $tenant->id) ->find($payload['event_id']); if (! $event) { return ApiError::response( 'export_event_missing', 'Event not found', 'The selected event does not exist for this tenant.', Response::HTTP_NOT_FOUND ); } } $hasInProgress = DataExport::query() ->where('tenant_id', $tenant->id) ->whereIn('status', [DataExport::STATUS_PENDING, DataExport::STATUS_PROCESSING]) ->exists(); if ($hasInProgress) { return ApiError::response( 'export_in_progress', 'Export already in progress', 'Please wait for the current export to finish before requesting another.', Response::HTTP_CONFLICT ); } $export = DataExport::query()->create([ 'user_id' => $user->id, 'tenant_id' => $tenant->id, 'event_id' => $event?->id, 'scope' => $scope, 'include_media' => (bool) ($payload['include_media'] ?? false), 'status' => DataExport::STATUS_PENDING, ]); GenerateDataExport::dispatch($export->id); return response()->json([ 'message' => 'Export started.', 'data' => [ 'id' => $export->id, 'scope' => $export->scope?->value ?? $export->scope, 'status' => $export->status, 'include_media' => (bool) $export->include_media, 'created_at' => optional($export->created_at)->toIso8601String(), ], ], Response::HTTP_ACCEPTED); } public function download(Request $request, DataExport $export): StreamedResponse|JsonResponse { $tenant = $this->resolveTenant($request); if ((int) $export->tenant_id !== (int) $tenant->id) { return ApiError::response( 'export_not_found', 'Export not found', 'The requested export is not available for this tenant.', Response::HTTP_NOT_FOUND ); } if (! $export->isReady() || $export->hasExpired() || ! $export->path) { return ApiError::response( 'export_not_ready', 'Export not ready', 'The export is not ready or has expired.', Response::HTTP_BAD_REQUEST ); } $disk = 'local'; if (! Storage::disk($disk)->exists($export->path)) { return ApiError::response( 'export_missing', 'Export not found', 'The export archive could not be located.', Response::HTTP_NOT_FOUND ); } return Storage::disk($disk)->download( $export->path, sprintf('fotospiel-data-export-%s.zip', $export->created_at?->format('Ymd') ?? now()->format('Ymd')), [ 'Cache-Control' => 'private, no-store', ] ); } private function resolveTenant(Request $request): Tenant { $tenant = $request->attributes->get('tenant'); if ($tenant instanceof Tenant) { return $tenant; } $tenantId = $request->attributes->get('tenant_id') ?? $request->attributes->get('current_tenant_id') ?? $request->user()?->tenant_id; if ($tenantId) { $tenant = Tenant::query()->find($tenantId); if ($tenant) { $request->attributes->set('tenant', $tenant); return $tenant; } } abort(401, 'Tenant context missing.'); } }