118 lines
7.6 KiB
PHP
118 lines
7.6 KiB
PHP
<?php
|
|
|
|
// Exclusive QA preparation; never prints credentials, MFA secrets, cookies or codes.
|
|
use App\Models\User;
|
|
use App\Services\OpportunityManagementInstallation;
|
|
use App\Services\OpportunityResourceInstallation;
|
|
use App\Services\PortalProfileAccess;
|
|
use App\Services\TerritoryGeometryImport;
|
|
use App\Services\VirtualDeskInstallation;
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Str;
|
|
|
|
$root = dirname(__DIR__);
|
|
require $root.'/vendor/autoload.php';
|
|
require __DIR__.'/portal-qa-environment.php';
|
|
$target = portalQaEnvironment();
|
|
$app = require $root.'/bootstrap/app.php';
|
|
$app->make(Kernel::class)->bootstrap();
|
|
$identity = portalQaAssertTarget($target);
|
|
$directory = $root.'/storage/app/private/investmoz-v2/portal-audit';
|
|
$private = $directory.'/profiles-v1-actors-private.json';
|
|
$baselinePath = $directory.'/profiles-v1-before-qa.json';
|
|
|
|
function snapshot(array $tables, ?array $baseline = null): array
|
|
{
|
|
$result = [];
|
|
foreach ($tables as $table) {
|
|
$columns = $baseline[$table]['columns'] ?? Schema::getColumnListing($table);
|
|
$projection = implode(',', array_map(fn ($c) => '"'.$c.'"', $columns));
|
|
$identifier = in_array('id', $columns, true) ? 'id::text' : 'NULL::text';
|
|
$rows = DB::select('SELECT '.$identifier.' AS id, row_to_json(t)::text AS row FROM (SELECT '.$projection.' FROM "'.$table.'") t ORDER BY row_to_json(t)::text');
|
|
$result[$table] = ['columns' => $columns, 'rows' => []];
|
|
foreach ($rows as $row) {
|
|
$digest = hash('sha256', $row->row);
|
|
$result[$table]['rows'][$row->id ?? $digest] = $digest;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
$tables = ['users', 'opportunities', 'organizations', 'memberships', 'documents', 'opportunity_projects', 'opportunity_clusters', 'opportunity_images', 'opportunity_documents', 'opportunity_locations', 'geo_locations', 'geo_geometries', 'desk_cases', 'desk_requests', 'investment_dossiers', 'investor_journeys', 'journey_steps', 'audit_events'];
|
|
$tables = array_values(array_filter($tables, fn ($t) => Schema::hasTable($t)));
|
|
$mode = $argv[1] ?? 'inspect';
|
|
if ($mode === 'prepare') {
|
|
if (! is_file($baselinePath)) {
|
|
file_put_contents($baselinePath, json_encode(['target' => $target, 'tables' => snapshot($tables), 'active_manifest_sha256' => hash_file('sha256', $root.'/public/build/manifest.json')], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
|
}
|
|
if (! Schema::hasTable('portal_role_assignments')) {
|
|
config(['portal_profiles.authenticated_installation' => true]);
|
|
throw_unless(Artisan::call('migrate', ['--path' => 'database/migrations/2026_10_10_040000_create_portal_profile_authorization.php', '--force' => true]) === 0, RuntimeException::class, 'QA profile migration failed');
|
|
}
|
|
$actors = is_file($private) ? json_decode(file_get_contents($private), true, 512, JSON_THROW_ON_ERROR) : [];
|
|
foreach (['public', 'promoter', 'colleague', 'outsider', 'validator', 'publisher', 'auditor', 'super_first', 'super_second', 'bootstrap', 'replacement', 'multiple'] as $profile) {
|
|
if (isset($actors[$profile])) {
|
|
$user = User::findOrFail($actors[$profile]['id']);
|
|
throw_unless($user->email === $actors[$profile]['email'] && str_ends_with($user->email, '@example.invalid'), RuntimeException::class, 'Synthetic actor identity mismatch');
|
|
|
|
continue;
|
|
}
|
|
$email = 'profiles-v1-'.$profile.'@example.invalid';
|
|
throw_if(User::where('email', $email)->exists(), RuntimeException::class, 'Existing actor has no private QA receipt; do not rotate it implicitly');
|
|
$password = Str::password(40);
|
|
$user = User::create(['name' => 'QA profiles v1 '.$profile, 'email' => $email, 'password' => Hash::make($password)]);
|
|
$user->forceFill(['email_verified_at' => now()])->save();
|
|
$actors[$profile] = ['id' => $user->id, 'email' => $email, 'password' => $password];
|
|
file_put_contents($private, json_encode($actors, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
|
}
|
|
foreach (['organization' => ['promoter', 'colleague', 'multiple'], 'other_organization' => ['outsider']] as $label => $members) {
|
|
$orgPath = $directory.'/profiles-v1-organizations-qa.json';
|
|
$organizations = is_file($orgPath) ? json_decode(file_get_contents($orgPath), true, 512, JSON_THROW_ON_ERROR) : [];
|
|
if (! isset($organizations[$label])) {
|
|
$organizations[$label] = (string) Str::uuid();
|
|
DB::table('organizations')->insert(['id' => $organizations[$label], 'source_system' => 'profiles-v1-qa', 'name' => 'Synthetic QA '.$label, 'organization_type' => 'private']);
|
|
file_put_contents($orgPath, json_encode($organizations, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
|
}
|
|
foreach ($members as $member) {
|
|
if (! DB::table('memberships')->where('organization_id', $organizations[$label])->where('user_id', $actors[$member]['id'])->exists()) {
|
|
DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'source_system' => 'profiles-v1-qa', 'organization_id' => $organizations[$label], 'user_id' => $actors[$member]['id'], 'role' => 'officer']);
|
|
}
|
|
}
|
|
}
|
|
echo json_encode(['target' => $identity, 'migration_installed_in_qa' => true, 'synthetic_actors' => array_map(fn ($a) => $a['id'], $actors), 'active_writes' => 0, 'mfa' => 'Enroll through authenticated Fortify; no direct MFA injection']).PHP_EOL;
|
|
exit;
|
|
}
|
|
if ($mode === 'verify') {
|
|
$baseline = json_decode(file_get_contents($baselinePath), true, 512, JSON_THROW_ON_ERROR);
|
|
$current = snapshot(array_keys($baseline['tables']), $baseline['tables']);
|
|
$preserved = [];
|
|
foreach ($baseline['tables'] as $table => $proof) {
|
|
$preserved[$table] = count(array_diff_assoc($proof['rows'], $current[$table]['rows'])) === 0;
|
|
}
|
|
$proof = ['target' => $identity, 'previous_rows_preserved_by_id_and_original_columns' => $preserved,
|
|
'captured_at' => gmdate('c'), 'schema_installed' => Schema::hasTable('portal_role_assignments'),
|
|
'fingerprints' => [
|
|
'profiles' => hash('sha256', json_encode(array_map(fn ($path) => hash_file('sha256', $path), PortalProfileAccess::fingerprintFiles()), JSON_THROW_ON_ERROR)),
|
|
'management' => app(OpportunityManagementInstallation::class)->fingerprint(),
|
|
'resources' => app(OpportunityResourceInstallation::class)->fingerprint(),
|
|
'desk_path' => app(VirtualDeskInstallation::class)->fingerprint(),
|
|
'geometry' => app(TerritoryGeometryImport::class)->fingerprint(),
|
|
],
|
|
'active_manifest_preserved' => hash_file('sha256', $root.'/public/build/manifest.json') === $baseline['active_manifest_sha256'],
|
|
'mfa18_unchanged' => $preserved['users'], 'pair' => Schema::hasTable('portal_superuser_pair') ? DB::table('portal_superuser_pair')->first() : null,
|
|
'active_writes' => 0, 'browser_visual_observed' => false];
|
|
throw_unless(! in_array(false, $preserved, true) && $proof['active_manifest_preserved'], RuntimeException::class, 'Baseline preservation failed');
|
|
file_put_contents($directory.'/profiles-v1-preservation-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
|
echo json_encode($proof, JSON_THROW_ON_ERROR).PHP_EOL;
|
|
exit;
|
|
}
|
|
DB::beginTransaction();
|
|
DB::statement('SET TRANSACTION READ ONLY');
|
|
echo json_encode(['target' => $identity, 'schema_tables' => Schema::getTableListing(), 'opportunity_columns' => Schema::getColumnListing('opportunities'), 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL;
|
|
DB::rollBack();
|