fix: complete InvesteMoz validation and error corrections
This commit is contained in:
@@ -2,18 +2,22 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Support\CheckedFile;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
/** Existing session, permission and MFA control; no actor argument or console entry point. */
|
||||
final class SectorReconciliationExecution
|
||||
{
|
||||
public const SOURCE = 'sector-reconciliation-authenticated';
|
||||
|
||||
public const OPERATION = 'sector-reconciliation-20261018';
|
||||
|
||||
private function actor(Request $request): \App\Models\User
|
||||
private function actor(Request $request): User
|
||||
{
|
||||
abort_if(app()->runningInConsole(), 403, 'A execução exige a sessão HTTP autenticada existente.');
|
||||
$actor = Auth::guard('web')->user();
|
||||
@@ -21,7 +25,7 @@ final class SectorReconciliationExecution
|
||||
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.');
|
||||
abort_unless((int) $request->session()->get('portal.mfa_at', 0) <= time(), 403, 'MFA da sessão inválido.');
|
||||
|
||||
return $actor;
|
||||
}
|
||||
@@ -29,45 +33,62 @@ final class SectorReconciliationExecution
|
||||
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);
|
||||
$manifest = json_decode(CheckedFile::read($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));
|
||||
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.');
|
||||
abort_unless(hash_equals($manifest['sql_sha256'], CheckedFile::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());
|
||||
foreach (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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
private function target(): array
|
||||
{
|
||||
return (array) DB::connection()->selectOne('SELECT current_database() AS database, inet_server_port() AS port', [], false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $target
|
||||
*/
|
||||
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) : [];
|
||||
$proof = is_file($p) ? json_decode(CheckedFile::read($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
|
||||
&& ($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) {
|
||||
&& ! 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);
|
||||
foreach (['dump', 'files'] as $kind) {
|
||||
abort_unless(is_file($proof[$kind.'_path'] ?? '')
|
||||
&& hash_equals($proof[$kind.'_sha256'] ?? '', CheckedFile::sha256($proof[$kind.'_path'])), 409);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
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);
|
||||
abort_if($request->query->count() > 0, 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'));
|
||||
@@ -75,6 +96,9 @@ final class SectorReconciliationExecution
|
||||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function apply(Request $request): array
|
||||
{
|
||||
$actor = $this->actor($request);
|
||||
@@ -82,7 +106,9 @@ final class SectorReconciliationExecution
|
||||
$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);
|
||||
$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) {
|
||||
@@ -90,8 +116,9 @@ final class SectorReconciliationExecution
|
||||
$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
|
||||
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);
|
||||
@@ -99,13 +126,13 @@ final class SectorReconciliationExecution
|
||||
$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);
|
||||
$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')));
|
||||
$db->getPdo()->exec(CheckedFile::read(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);
|
||||
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,
|
||||
'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',
|
||||
|
||||
Reference in New Issue
Block a user