121 lines
6.5 KiB
PHP
121 lines
6.5 KiB
PHP
<?php
|
|
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
require dirname(__DIR__).'/vendor/autoload.php';
|
|
$root = dirname(__DIR__);
|
|
if (! in_array('--apply-authorized', $argv, true)) {
|
|
throw new LogicException('Explicit authorized installation required.');
|
|
}
|
|
$s = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/payables-demo-compras.json'), true, 32, JSON_THROW_ON_ERROR);
|
|
if (($s['APP_ENV'] ?? '') !== 'payable-testing' || ($s['DB_HOST'] ?? '') !== '127.0.0.1' || realpath($s['HOST_ROOT'] ?? '') !== realpath($root) || ! preg_match('/^compras_receipts_test_([a-f0-9]{12})$/D', $s['DB_DATABASE'] ?? '', $m) || $s['DB_USERNAME'] !== 'payable_qa_'.$m[1]) {
|
|
throw new LogicException('Wrong pilot destination.');
|
|
}
|
|
$app = require $root.'/bootstrap/app.php';
|
|
$app->make(Kernel::class)->bootstrap();
|
|
$owner = DB::connection('pgsql')->getConfig();
|
|
unset($owner['name']);
|
|
if ($owner['host'] !== '127.0.0.1') {
|
|
throw new LogicException('Local server required.');
|
|
}
|
|
$qa = [...$owner, 'url' => null, 'database' => $s['DB_DATABASE'], 'username' => $s['DB_USERNAME'], 'password' => $s['DB_PASSWORD']];
|
|
config(['database.connections.receipt_install' => $qa, 'database.connections.receipt_owner' => [...$owner, 'url' => null, 'database' => 'postgres']]);
|
|
DB::setDefaultConnection('receipt_install');
|
|
$db = DB::connection();
|
|
$admin = DB::connection('receipt_owner');
|
|
$identity = $db->selectOne('select current_database() as db, current_user as role');
|
|
if ($identity->db !== $s['DB_DATABASE'] || $identity->role !== $s['DB_USERNAME']) {
|
|
throw new LogicException('Identity mismatch.');
|
|
}
|
|
$proofPath = $root.'/docs/proofs/receipt-block-installation.json';
|
|
if (file_exists($proofPath)) {
|
|
throw new LogicException('Installation proof exists; inspect before repeating.');
|
|
}
|
|
$tables = array_column($db->select("select tablename from pg_tables where schemaname='public' order by tablename"), 'tablename');
|
|
if (in_array('receipt_documents', $tables, true)) {
|
|
throw new LogicException('Document table already exists.');
|
|
}
|
|
$fingerprint = function ($connection, $tables) {
|
|
$out = [];
|
|
foreach ($tables as $table) {
|
|
$rows = $connection->select('select row_to_json(t)::text as row from "'.str_replace('"', '""', $table).'" t order by row_to_json(t)::text');
|
|
$out[$table] = hash('sha256', json_encode(array_column($rows, 'row'), JSON_THROW_ON_ERROR));
|
|
}
|
|
|
|
return $out;
|
|
};
|
|
$pg = function ($binary, $args, $connection) {
|
|
$p = new Process(['C:/Program Files/PostgreSQL/17/bin/'.$binary.'.exe', ...$args], null, ['PGHOST' => $connection['host'], 'PGPORT' => (string) $connection['port'], 'PGUSER' => $connection['username'], 'PGPASSWORD' => $connection['password']], timeout: 120);
|
|
$p->run();
|
|
if (! $p->isSuccessful()) {
|
|
throw new RuntimeException('Backup/restore failed.');
|
|
}
|
|
};
|
|
$restore = 'compras_pilot_restore_'.bin2hex(random_bytes(6));
|
|
$created = false;
|
|
try {
|
|
$db->beginTransaction();
|
|
$db->statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
|
|
$baseline = $fingerprint($db, $tables);
|
|
$directory = 'C:/laragon/data/sgf-tempo/backups/compras-receipt-documents-'.gmdate('Ymd-His').'-'.bin2hex(random_bytes(3));
|
|
if (! mkdir($directory, 0700, true)) {
|
|
throw new RuntimeException('Backup directory unavailable.');
|
|
}
|
|
$dump = $directory.'/before.dump';
|
|
$snapshot = $db->selectOne('select pg_export_snapshot() as snapshot')->snapshot;
|
|
$pg('pg_dump', ['--format=custom', '--snapshot='.$snapshot, '--file='.$dump, '--dbname='.$qa['database']], $qa);
|
|
$db->commit();
|
|
$admin->statement('CREATE DATABASE "'.$restore.'"');
|
|
$created = true;
|
|
$pg('pg_restore', ['--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$restore, $dump], $owner);
|
|
config(['database.connections.receipt_restore' => [...$owner, 'url' => null, 'database' => $restore]]);
|
|
if ($fingerprint(DB::connection('receipt_restore'), $tables) !== $baseline) {
|
|
throw new LogicException('Restored backup differs.');
|
|
}
|
|
$disk = Storage::disk('receipt_documents');
|
|
$probe = '.installation-'.bin2hex(random_bytes(8));
|
|
try {
|
|
$disk->put($probe, 'private-storage-probe');
|
|
if ($disk->get($probe) !== 'private-storage-probe') {
|
|
throw new RuntimeException('Storage unavailable.');
|
|
}
|
|
} finally {
|
|
$disk->delete($probe);
|
|
}
|
|
$migration = 'database/migrations/2026_11_03_000002_create_receipt_documents.php';
|
|
$db->transaction(function () use ($db, $tables, $baseline, $fingerprint, $migration) {
|
|
foreach ($tables as $table) {
|
|
$db->statement('LOCK TABLE "'.str_replace('"', '""', $table).'" IN SHARE MODE');
|
|
}
|
|
if ($fingerprint($db, $tables) !== $baseline) {
|
|
throw new LogicException('Pilot changed after backup; abort.');
|
|
}
|
|
if (Artisan::call('migrate', ['--database' => 'receipt_install', '--path' => $migration, '--force' => true]) !== 0) {
|
|
throw new RuntimeException('Target migration failed.');
|
|
}
|
|
$protected = array_values(array_diff($tables, ['migrations']));
|
|
if ($fingerprint($db, $protected) !== array_intersect_key($baseline, array_flip($protected))) {
|
|
throw new LogicException('Business data changed.');
|
|
}
|
|
if ($db->table('receipt_documents')->count() !== 0 || $db->table('migrations')->where('migration', '2026_11_03_000001_add_two_factor_columns_to_users')->exists()) {
|
|
throw new LogicException('Unexpected installation state.');
|
|
}
|
|
});
|
|
$proof = ['checked_at' => gmdate('c'), 'environment' => 'http://127.0.0.1:8005', 'result' => 'passed', 'authorization' => 'User explicitly authorized receipt document block installation', 'migration' => $migration, 'backup' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'restored_tables_checked' => count($tables), 'protected_tables_checked' => count($tables) - 1, 'business_data_unchanged' => true, 'documents_created' => 0, 'mfa_enabled' => false, 'restore_database_removed' => false];
|
|
} finally {
|
|
if ($db->transactionLevel() > 0) {
|
|
$db->rollBack();
|
|
}
|
|
DB::purge('receipt_restore');
|
|
if ($created) {
|
|
$admin->statement('DROP DATABASE "'.$restore.'" WITH (FORCE)');
|
|
}
|
|
}
|
|
$proof['restore_database_removed'] = true;
|
|
file_put_contents($proofPath, json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL);
|
|
echo 'Receipt documents installed; backup restored and business data preserved.'.PHP_EOL;
|