oportunidades 5
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-10 14:15:28 +02:00
parent 6ddafc95ba
commit 01405cbf8a
144 changed files with 28903 additions and 1103 deletions
@@ -3,21 +3,22 @@
namespace App\Http\Controllers;
use App\Models\Opportunity;
use App\Services\CountryCatalogue;
use App\Services\InvestmentGeography;
use App\Services\OpportunityImageProcessor;
use App\Services\OpportunityManagement;
use App\Services\OpportunityResourceInstallation;
use App\Services\OpportunityResources;
use App\Services\PortalProfileAccess;
use App\Services\VerifiedDocumentBinary;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class OpportunityResourceController extends Controller
{
@@ -40,9 +41,12 @@ class OpportunityResourceController extends Controller
public function index(int $id, OpportunityResources $service): Response
{
$opportunity = Opportunity::withTrashed()->findOrFail($id);
app(PortalProfileAccess::class)->authorize(request()->user(), 'view', $opportunity);
Inertia::share(['basePath' => str_starts_with(request()->path(), 'minhas-oportunidades') ? '/minhas-oportunidades' : '/management/opportunities', 'readOnly' => ! app(PortalProfileAccess::class)->canEdit(request()->user(), $opportunity)]);
abort_unless($service->installed(), 503, 'Recursos ainda não instalados; consulte o plano autenticado.');
$resources = [];
Inertia::share('imageProcessingAvailable', app(OpportunityImageProcessor::class)->available());
Inertia::share('countries', app(CountryCatalogue::class)->all());
foreach (OpportunityResources::KINDS as $kind) {
$resources[$kind] = $service->listing($id, $kind);
}
@@ -54,7 +58,7 @@ class OpportunityResourceController extends Controller
$image->derivativePreviews = [];
foreach ($image->payload['derivatives'] ?? [] as $derived) {
if (DB::table('documents')->where('id', $derived['document_id'])->where('status', 'active')->where('malware_scan_status', 'clean')->whereColumn('sha256', 'malware_scanned_sha256')->exists()) {
$image->derivativePreviews[] = [...$derived, 'url' => URL::temporarySignedRoute('management.opportunity-image-preview', now()->addMinutes(10), ['id' => $id, 'resourceId' => $image->id, 'documentId' => $derived['document_id']])];
$image->derivativePreviews[] = [...$derived, 'url' => URL::temporarySignedRoute(str_starts_with(request()->path(), 'minhas-oportunidades') ? 'promoter.opportunity-image-preview' : 'management.opportunity-image-preview', now()->addMinutes(10), ['id' => $id, 'resourceId' => $image->id, 'documentId' => $derived['document_id']])];
}
}
}
@@ -87,7 +91,7 @@ class OpportunityResourceController extends Controller
return response()->json(['message' => 'Derivadas WebP geradas sem EXIF original; scan independente e revisão de divulgação pendentes.']);
}
public function previewImage(int $id, string $resourceId, string $documentId): \Illuminate\Http\Response
public function previewImage(int $id, string $resourceId, string $documentId): \Illuminate\Http\Response|BinaryFileResponse
{
$o = Opportunity::findOrFail($id);
abort_if($o->status === 'archived', 404);
@@ -97,26 +101,20 @@ class OpportunityResourceController extends Controller
abort_unless(in_array($documentId, array_column($metadata['derivatives'] ?? [], 'document_id'), true), 404);
$doc = DB::table('documents')->where('id', $documentId)->whereNull('deleted_at')->first();
abort_unless($doc && $doc->status === 'active' && $doc->malware_scan_status === 'clean' && hash_equals(trim($doc->sha256), (string) $doc->malware_scanned_sha256), 423, 'Scan da derivada pendente.');
$bytes = Crypt::decryptString(Storage::disk('local')->get($doc->storage_key));
abort_unless(hash_equals(trim($doc->sha256), hash('sha256', $bytes)), 409, 'Integridade divergente.');
return response($bytes)->header('Content-Type', 'image/webp')->header('X-Content-Type-Options', 'nosniff')->header('Cache-Control', 'private,no-store');
return app(VerifiedDocumentBinary::class)->respond($doc, true);
}
public function download(int $id, string $kind, string $resourceId, OpportunityResources $service): \Illuminate\Http\Response
public function download(int $id, string $kind, string $resourceId, OpportunityResources $service): \Illuminate\Http\Response|BinaryFileResponse
{
abort_unless(in_array($kind, ['documents', 'images'], true), 404);
$opportunity = Opportunity::findOrFail($id);
abort_if($opportunity->status === 'archived', 404);
$record = DB::table($service->table($kind).' as r')->join('documents as d', 'd.id', '=', 'r.document_id')->where('r.id', $resourceId)->where('r.opportunity_id', $id)->whereNull('r.deleted_at')->whereNull('d.deleted_at')->first(['d.*']);
abort_unless($record && $record->status === 'active' && $record->malware_scan_status === 'clean' && hash_equals(trim($record->sha256), (string) $record->malware_scanned_sha256), 423, 'Scan pendente, inconclusivo ou inseguro. O ficheiro permanece em quarentena.');
$disk = Storage::disk('local');
$path = $disk->path($record->storage_key);
abort_unless($disk->exists($record->storage_key), 409, 'Ficheiro indisponível.');
$bytes = $record->storage_encryption === 'laravel' ? Crypt::decryptString($disk->get($record->storage_key)) : $disk->get($record->storage_key);
abort_unless(hash_equals(trim($record->sha256), hash('sha256', $bytes)), 409, 'Integridade do ficheiro divergente.');
abort_unless($record !== null, 404, 'Recurso indisponível ou excluído logicamente.');
abort_unless($record->status === 'active' && $record->malware_scan_status === 'clean' && hash_equals(trim($record->sha256), (string) $record->malware_scanned_sha256), 423, 'Scan pendente, inconclusivo ou inseguro. O ficheiro permanece em quarentena.');
return response($bytes)->header('Content-Type', $record->media_type)->header('Content-Disposition', HeaderUtils::makeDisposition('attachment', $record->file_name, 'document'))->header('X-Content-Type-Options', 'nosniff')->header('Cache-Control', 'no-store');
return app(VerifiedDocumentBinary::class)->respond($record);
}
/** @param array<string,list<\stdClass>> $resources