find($id, $request); // Same private document infrastructure and technical file limit as investment attachments. $request->validate(['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,image/png,image/jpeg', 'max:10240']]); $file = $request->file('file'); $hash = hash_file('sha256', $file->getRealPath()); abort_unless(is_string($hash), 422, 'Não foi possível verificar o conteúdo do anexo.'); $existing = DB::table('documents')->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->where('sha256', $hash)->whereNull('deleted_at')->first(); if ($existing) { return response()->json(['data' => $this->metadata($id, $existing)], 200)->header('Cache-Control', 'private, no-store'); } $documentId = (string) Str::uuid(); $key = $file->storeAs('simulation-offers/'.$id, $documentId, 'local'); abort_unless(is_string($key), 500); try { DB::table('documents')->insert(['id' => $documentId, 'source_system' => 'simulation-fiscal-offer', 'external_id' => $id, 'file_name' => basename($file->getClientOriginalName()), 'media_type' => $file->getMimeType(), 'storage_key' => $key, 'sha256' => $hash, 'size_bytes' => $file->getSize(), 'uploaded_by' => $request->user()->id, 'status' => 'quarantined', 'data_classification' => 'confidential']); } catch (\Throwable $e) { Storage::disk('local')->delete($key); throw $e; } ScanDocumentForMalware::dispatch($documentId)->afterCommit(); return response()->json(['data' => $this->metadata($id, DB::table('documents')->where('id', $documentId)->first())], 201)->header('Cache-Control', 'private, no-store'); } public function index(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse { abort_unless(Str::isUuid($id), 404); $drafts->find($id, $request); $documents = DB::table('documents')->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->whereNull('deleted_at')->get(); return response()->json(['data' => $documents->map(fn ($doc) => $this->metadata($id, $doc))])->header('Cache-Control', 'private, no-store'); } public function download(Request $request, string $id, string $document, SimulationFormDrafts $drafts): BinaryFileResponse { abort_unless(Str::isUuid($id) && Str::isUuid($document), 404); $drafts->find($id, $request); $doc = DB::table('documents')->where('id', $document)->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->whereNull('deleted_at')->first(); abort_unless($doc !== null, 404); abort_unless($doc->status === 'active' && $doc->malware_scan_status === 'clean' && hash_equals(trim($doc->sha256), (string) $doc->malware_scanned_sha256), 409, 'Anexo conservado; descarga disponível após análise antimalware concluída.'); $root = realpath(Storage::disk('local')->path('simulation-offers/'.$id)); $path = realpath(Storage::disk('local')->path($doc->storage_key)); abort_unless($root && $path && str_starts_with(strtolower($path), strtolower($root.DIRECTORY_SEPARATOR)) && is_file($path), 404); $hash = hash_file('sha256', $path); abort_unless(is_string($hash) && hash_equals(trim($doc->sha256), $hash), 409, 'Conteúdo do anexo diverge do hash registado.'); return response()->download($path, basename($doc->file_name), ['Cache-Control' => 'private, no-store', 'X-Content-Type-Options' => 'nosniff']); } /** @return array{id: string, draftId: string, name: string, sha256: string, scan: string, url: string} */ private function metadata(string $id, stdClass $doc): array { return ['id' => $doc->id, 'draftId' => $id, 'name' => $doc->file_name, 'sha256' => trim($doc->sha256), 'scan' => $doc->malware_scan_status, 'url' => '/api/v1/simulation-drafts/'.$id.'/offer-documents/'.$doc->id]; } }