runningInConsole(), 403, 'A execução exige a sessão HTTP autenticada existente.'); $actor = Auth::guard('web')->user(); abort_unless($request->hasSession() && $actor && $request->user()?->id === $actor->id, 401); abort_unless($actor->hasVerifiedEmail() && $actor->can('manage-portal'), 403); // Same installation permission and Fortify session MFA used by 140000/150000. app(PortalProfileInstallation::class)->authorize($actor, true); abort_unless((int)$request->session()->get('portal.mfa_at', 0) <= time(), 403, 'MFA da sessão inválido.'); return $actor; } public function revision(): string { $file = database_path('data/sector-reconciliation-execution.json'); $manifest = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); $files = ['database/data/sector-reconciliation-execution.json' => hash_file('sha256', $file)]; foreach ($manifest['files'] as $path) $files[$path] = hash_file('sha256', base_path($path)); $sql = database_path('data/sector-reconciliation-20261018.sql'); abort_unless(hash_equals($manifest['sql_sha256'], hash_file('sha256', $sql)), 409, 'SQL diferente da revisão delimitada.'); $files['database/data/sector-reconciliation-20261018.sql'] = hash_file('sha256', $sql); foreach (\Illuminate\Support\Facades\File::allFiles(public_path('build')) as $build) $files['public/build/'.$build->getRelativePathname()] = hash_file('sha256', $build->getPathname()); ksort($files); return hash('sha256', json_encode($files, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); } private function target(): array { return (array) DB::connection()->selectOne('SELECT current_database() AS database, inet_server_port() AS port', [], false); } private function readiness(string $revision, array $target): void { $p = storage_path('app/private/investmoz-v2/sector-reconciliation/readiness.json'); $proof = is_file($p) ? json_decode(file_get_contents($p), true, 512, JSON_THROW_ON_ERROR) : []; abort_unless(($proof['operation'] ?? '') === self::OPERATION && ($proof['revision'] ?? '') === $revision && ($proof['target'] ?? []) === $target && !empty($proof['authorization_record']), 409, 'Préflight/autorização da revisão e destino necessários.'); $isolated = app()->environment('testing') && (int)$target['port'] === 55441 && str_ends_with($target['database'], '_test') && config('database.connections.pgsql.host') === '127.0.0.1' && !config('database.connections.pgsql.url'); if (!$isolated) { abort_unless(($proof['restore_verified'] ?? false) && ($proof['restored_boot_verified'] ?? false) && strtotime($proof['verified_at'] ?? '') >= time() - 86400 && strtotime($proof['verified_at'] ?? '') <= time(), 409, 'Backup/restauro fresco do procedimento existente necessário.'); foreach (['dump', 'files'] as $kind) abort_unless(is_file($proof[$kind.'_path'] ?? '') && hash_equals($proof[$kind.'_sha256'] ?? '', hash_file('sha256', $proof[$kind.'_path'])), 409); } } public function preview(Request $request): array { $actor = $this->actor($request); abort_if($request->query->count(), 422, 'Ator, revisão ou opções por URL recusados.'); $revision = $this->revision(); $target = $this->target(); $this->readiness($revision, $target); $state = ['operation' => self::OPERATION, 'revision' => $revision, 'target' => $target, 'actor' => ['id' => $actor->id, 'name' => $actor->name], 'migrations' => [], 'ready' => true]; $state['hash'] = hash_hmac('sha256', json_encode([$actor->id, $request->session()->getId(), $state], JSON_THROW_ON_ERROR), config('app.key')); return $state; } public function apply(Request $request): array { $actor = $this->actor($request); abort_if(array_diff(array_keys($request->all()), ['hash', 'operation_key', 'confirm', '_token']) || $request->query->count(), 422, 'Ator, revisão, SQL ou parâmetros arbitrários recusados.'); $data = Validator::make($request->all(), ['hash' => 'required|string|size:64', 'operation_key' => 'required|uuid', 'confirm' => 'required|accepted'])->validate(); $db = DB::connection(); abort_unless($db->transactionLevel() === 0, 409, 'A operação deve possuir a sua transação.'); $revision = $this->revision(); $target = $this->target(); $this->readiness($revision, $target); $requestHash = hash('sha256', json_encode($data, JSON_THROW_ON_ERROR)); $result = $db->transaction(function () use ($db, $actor, $data, $request, $revision, $target, $requestHash) { $db->selectOne('SELECT pg_advisory_xact_lock(181026, 4)', [], false); $old = $db->table('audit_events')->useWritePdo()->where('source_system', self::SOURCE)->where('correlation_id', $data['operation_key'])->first(); if ($old) { $saved = json_decode($old->after_data, true, 512, JSON_THROW_ON_ERROR); abort_unless((int)$old->actor_user_id === $actor->id && ($saved['revision'] ?? '') === $revision && hash_equals($saved['request_hash'], $requestHash), 409); return [...$saved['receipt'], 'replayed' => true]; } $plan = $this->preview($request); abort_unless(hash_equals($plan['hash'], $data['hash']), 409, 'Prévia diferente da sessão/revisão atual.'); $before = $db->selectOne("SELECT pg_backend_pid() AS pid, txid_current()::text AS txid, NULLIF(current_setting('investmoz.actor_user_id',true),'') AS actor", [], false); abort_unless($before->actor === null, 409, 'Ligação contém um ator anterior; operação recusada.'); // true means SET LOCAL. Same write connection and transaction execute every statement. $db->selectOne("SELECT set_config('investmoz.actor_user_id',?,true), set_config('investmoz.operation_key',?,true), set_config('investmoz.authorized_revision',?,true)", [(string)$actor->id, $data['operation_key'], $revision], false); $oldDataEvent = $db->table('audit_events')->useWritePdo()->where('source_system', self::OPERATION)->exists(); $db->getPdo()->exec(file_get_contents(database_path('data/sector-reconciliation-20261018.sql'))); $scope = $db->selectOne("SELECT pg_backend_pid() AS pid, txid_current()::text AS txid, current_setting('investmoz.actor_user_id') AS actor", [], false); abort_unless($scope->pid === $before->pid && $scope->txid === $before->txid && (int)$scope->actor === $actor->id, 500); $receipt = ['operation' => self::OPERATION, 'operation_key' => $data['operation_key'], 'revision' => $revision, 'actor_user_id' => $actor->id, 'target' => $target, 'changed' => !$oldDataEvent, 'opportunity_id' => 2, 'sector_id' => 8, 'replayed' => false, 'connection_pid' => $scope->pid, 'transaction_id' => $scope->txid]; $db->table('audit_events')->insert(['source_system' => self::SOURCE, 'entity_type' => 'sector-reconciliation', 'action' => 'execute', 'actor_user_id' => $actor->id, 'correlation_id' => $data['operation_key'], 'data_classification' => 'internal', 'after_data' => json_encode(['operation' => self::OPERATION, 'revision' => $revision, 'request_hash' => $requestHash, 'receipt' => $receipt], JSON_THROW_ON_ERROR)]); return $receipt; }); $after = $db->selectOne("SELECT pg_backend_pid() AS pid, NULLIF(current_setting('investmoz.actor_user_id',true),'') AS actor, NULLIF(current_setting('investmoz.operation_key',true),'') AS operation, NULLIF(current_setting('investmoz.authorized_revision',true),'') AS revision", [], false); abort_unless($after->actor === null && $after->operation === null && $after->revision === null, 500, 'Contexto local não terminou com a transação.'); return [...$result, 'local_settings_cleared' => true, 'connection_after_commit_pid' => $after->pid]; } }