files() as $p) { $files[$p] = hash_file('sha256', base_path($p)); } foreach (File::allFiles(public_path('build')) as $p) { $files['public/build/'.$p->getRelativePathname()] = hash_file('sha256', $p->getPathname()); }ksort($files); return hash('sha256', json_encode($files, JSON_THROW_ON_ERROR)); } private function authorize(User $actor, bool $write = false): void { abort_unless($actor->hasVerifiedEmail() && $actor->can('manage-portal'), 403); if ($write) { app(PortalProfileInstallation::class)->authorize($actor, true); } } public function preview(User $actor): array { $this->authorize($actor); $target = (array) DB::selectOne('SELECT current_database() AS database,inet_server_port() AS port'); $code = $this->fingerprint(); $table = Schema::hasTable('investor_navigation_preferences'); $ledger = DB::table('migrations')->where('migration', InvestorContext::MIGRATION)->count(); $consistent = ($table && $ledger === 1) || (! $table && $ledger === 0); $reasons = []; if (! $consistent) { $reasons[] = 'Esquema e registo da migration divergentes; reconciliar sem recriar dados.'; } try { $this->authorize($actor, true); } catch (HttpException $e) { $reasons[] = $e->getMessage() ?: 'Elevação existente necessária para a instalação excecional.'; } $isolated = app()->environment('testing') && config('database.connections.pgsql.host') === '127.0.0.1' && (int) $target['port'] === 55441 && str_ends_with($target['database'], '_test') && ! config('database.connections.pgsql.url'); $proofPath = storage_path('app/private/investmoz-v2/investor-context/readiness.json'); $proof = is_file($proofPath) ? json_decode(file_get_contents($proofPath), true) : []; $fresh = ! app()->environment('testing') && ($proof['code_hash'] ?? '') === $code && ($proof['target'] ?? []) === $target && ($proof['authorization_record'] ?? '') !== '' && ($proof['migration'] ?? '') === InvestorContext::MIGRATION && ($proof['restore_verified'] ?? false) && ($proof['restored_boot_verified'] ?? false) && ($proof['compatibility_verified'] ?? false) && strtotime($proof['verified_at'] ?? '') >= time() - 86400 && strtotime($proof['verified_at'] ?? '') <= time(); foreach (['dump', 'files'] as $kind) { $p = $proof[$kind.'_path'] ?? ''; $fresh = $fresh && is_file($p) && hash_equals($proof[$kind.'_sha256'] ?? '', hash_file('sha256', $p)); } if (! $isolated && ! $fresh) { $reasons[] = 'Autorização específica, hashes e backup/restauro/arranque frescos desta candidata necessários.'; } $state = ['target' => $target, 'migration' => InvestorContext::MIGRATION, 'code_hash' => $code, 'installed' => $table && $ledger === 1, 'consistent' => $consistent, 'ready' => ! $reasons, 'reasons' => $reasons, 'business_records_changed' => 0]; return [...$state, 'hash' => hash_hmac('sha256', json_encode([$actor->id, session()->getId(), $state], JSON_THROW_ON_ERROR), config('app.key'))]; } public function apply(User $actor, array $input): array { $this->authorize($actor, true); abort_if(array_diff(array_keys($input), ['hash', 'operation_key', 'confirm', '_token']), 422, 'Migration, ator ou caminho arbitrário recusado.'); $data = Validator::make($input, ['hash' => 'required|string|size:64', 'operation_key' => 'required|uuid', 'confirm' => 'required|accepted'])->validate(); return DB::transaction(function () use ($actor, $data) { DB::select('SELECT pg_advisory_xact_lock(161026,150000)'); $hash = hash('sha256', json_encode($data, JSON_THROW_ON_ERROR)); $old = DB::table('audit_events')->where('source_system', self::SOURCE)->where('correlation_id', $data['operation_key'])->first(); if ($old) { $saved = json_decode($old->after_data, true); abort_unless((int) $old->actor_user_id === $actor->id && hash_equals($saved['request_hash'], $hash), 409); return [...$saved['result'], 'replayed' => true]; } $plan = $this->preview($actor); abort_unless(hash_equals($plan['hash'], $data['hash']), 409, 'Prévia alterada.'); abort_unless($plan['ready'], 422, implode(' ', $plan['reasons'])); if (! $plan['installed']) { config(['investor-context.authenticated_installation' => true]); try { abort_unless(Artisan::call('migrate', ['--path' => 'database/migrations/'.InvestorContext::MIGRATION.'.php', '--force' => true]) === 0, 500); } finally { config(['investor-context.authenticated_installation' => false]); } } abort_unless(Schema::hasTable('investor_navigation_preferences'), 500); $result = ['migration' => InvestorContext::MIGRATION, 'changed' => ! $plan['installed'], 'business_records_changed' => 0, 'replayed' => false]; DB::table('audit_events')->insert(['source_system' => self::SOURCE, 'entity_type' => 'navigation-preference', 'actor_user_id' => $actor->id, 'correlation_id' => $data['operation_key'], 'action' => 'install', 'after_data' => json_encode(['request_hash' => $hash, 'result' => $result], JSON_THROW_ON_ERROR)]); return $result; }); } }