Preserva base local aceite de identidade e acessos
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
param([ValidateSet('start','stop')][string]$Action = 'start')
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$sgfProject = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
if ($sgfProject -ne 'C:\laragon\www\sgf-tempo') { throw 'Unexpected project path.' }
|
||||
$sgfPhp = 'C:\laragon\tools\sgf-tempo\php-8.4.27\php.exe'
|
||||
$sgfRun = 'C:\laragon\data\sgf-tempo\run'
|
||||
$recordFile = Join-Path $sgfRun 'browser-proof.json'
|
||||
Set-Location -LiteralPath $sgfProject
|
||||
if ($Action -eq 'stop') {
|
||||
if (Test-Path -LiteralPath $recordFile) {
|
||||
$record = Get-Content -LiteralPath $recordFile -Raw | ConvertFrom-Json
|
||||
$process = Get-Process -Id $record.pid -ErrorAction SilentlyContinue
|
||||
if ($process) {
|
||||
if ($process.Path -ne $sgfPhp -or $process.StartTime.ToUniversalTime().ToString('o') -ne $record.started) { throw 'Process identity mismatch; no process stopped.' }
|
||||
Stop-Process -Id $process.Id
|
||||
}
|
||||
Remove-Item -LiteralPath $recordFile
|
||||
}
|
||||
$fixtureFile = 'C:\laragon\data\sgf-tempo\secrets\browser-fixture.json'
|
||||
if (Test-Path -LiteralPath $fixtureFile) { Remove-Item -LiteralPath $fixtureFile }
|
||||
Write-Output 'Browser proof server stopped. Synthetic data remains only in sgf_tempo_test until the next test run.'
|
||||
exit 0
|
||||
}
|
||||
if (Get-NetTCPConnection -LocalPort 8001 -State Listen -ErrorAction SilentlyContinue) { throw 'Port 8001 occupied; no process was stopped.' }
|
||||
if (Test-Path -LiteralPath $recordFile) { throw 'Browser proof record exists; stop that proof first.' }
|
||||
& $sgfPhp scripts/prepare-browser-fixture.php --env=testing
|
||||
if ($LASTEXITCODE) { throw 'Could not prepare synthetic fixtures.' }
|
||||
$utf8 = New-Object Text.UTF8Encoding $false
|
||||
$lines = Get-Content -LiteralPath '.env.testing' | Where-Object { $_ -notmatch '^(APP_ENV|APP_URL|SESSION_DRIVER|SESSION_COOKIE|SESSION_FILES|CACHE_STORE|CACHE_FILE_PATH|CACHE_LOCK_PATH)=' }
|
||||
$lines += @('APP_ENV=browser-testing','APP_URL=http://127.0.0.1:8001','SESSION_DRIVER=file','SESSION_COOKIE=sgf_browser_proof',
|
||||
'SESSION_FILES=C:/laragon/www/sgf-tempo/storage/framework/browser-proof/sessions', 'CACHE_STORE=file',
|
||||
'CACHE_FILE_PATH=C:/laragon/www/sgf-tempo/storage/framework/browser-proof/cache', 'CACHE_LOCK_PATH=C:/laragon/www/sgf-tempo/storage/framework/browser-proof/locks')
|
||||
[IO.File]::WriteAllLines((Join-Path $sgfProject '.env.browser-testing'), $lines, $utf8)
|
||||
$account = [Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
& icacls.exe '.env.browser-testing' /inheritance:r /grant:r "${account}:F" 'SYSTEM:F' | Out-Null
|
||||
New-Item -ItemType Directory -Path 'storage\framework\browser-proof\sessions','storage\framework\browser-proof\cache','storage\framework\browser-proof\locks' -Force | Out-Null
|
||||
$env:APP_ENV = 'browser-testing'
|
||||
try {
|
||||
if (Get-NetTCPConnection -LocalPort 8001 -State Listen -ErrorAction SilentlyContinue) { throw 'Port 8001 became occupied; no process stopped.' }
|
||||
$process = Start-Process -FilePath $sgfPhp -ArgumentList @('-S','127.0.0.1:8001','-t','public','scripts/router.php') -WorkingDirectory $sgfProject -WindowStyle Hidden -PassThru -RedirectStandardOutput "$sgfRun\browser-proof.stdout.log" -RedirectStandardError "$sgfRun\browser-proof.stderr.log"
|
||||
@{pid=$process.Id;started=$process.StartTime.ToUniversalTime().ToString('o')} | ConvertTo-Json | Set-Content -LiteralPath $recordFile -Encoding UTF8
|
||||
} finally { Remove-Item Env:APP_ENV -ErrorAction SilentlyContinue }
|
||||
Write-Output 'Browser proof: http://127.0.0.1:8001 — synthetic data only, isolated session/cache, CSRF enabled.'
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
if (! \App\Modules\Identity\Services\LocalDatabaseGuard::allows()) { fwrite(STDERR, "Invalid local destination.\n"); exit(1); }
|
||||
$tables = \Illuminate\Support\Facades\DB::select("SELECT relname, has_table_privilege(current_user, c.oid, 'SELECT') AS can_read, has_table_privilege(current_user, c.oid, 'INSERT') AS can_insert, has_table_privilege(current_user, c.oid, 'UPDATE') AS can_update FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND relname IN ('users','tenants','companies','company_memberships','permission_grants','identity_audit') ORDER BY relname");
|
||||
$sequences = \Illuminate\Support\Facades\DB::select("SELECT relname, has_sequence_privilege(current_user, c.oid, 'USAGE') AS can_use FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND relkind = 'S' ORDER BY relname");
|
||||
echo json_encode(['guard' => 'passed', 'tables' => $tables, 'sequences' => $sequences], JSON_PRETTY_PRINT).PHP_EOL;
|
||||
@@ -0,0 +1,59 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const base = 'http://127.0.0.1:8001';
|
||||
const fixture = JSON.parse(await fs.readFile('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json', 'utf8'));
|
||||
const results = [];
|
||||
function client() {
|
||||
const cookies = new Map();
|
||||
let csrf = ''; let context = '';
|
||||
return {
|
||||
cookies,
|
||||
async request(path, method = 'GET', body, {token = context, csrfHeader = true} = {}) {
|
||||
const headers = {Accept:'application/json', 'Content-Type':'application/json', Cookie:[...cookies].map(([k,v]) => `${k}=${v}`).join('; '), 'X-SGF-Context':token};
|
||||
if (csrfHeader) headers['X-CSRF-TOKEN'] = csrf;
|
||||
const response = await fetch(base+path, {method, headers, ...(body === undefined ? {} : {body:JSON.stringify(body)})});
|
||||
for (const cookie of response.headers.getSetCookie()) { const pair=cookie.split(';',1)[0]; const split=pair.indexOf('='); cookies.set(pair.slice(0,split),pair.slice(split+1)); }
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (data.csrf) csrf=data.csrf;
|
||||
if (data.context?.token) context=data.context.token;
|
||||
return {status:response.status,data};
|
||||
},
|
||||
async login(email) {
|
||||
assert.equal((await this.request('/api/session')).status,200);
|
||||
assert.equal((await this.request('/api/login','POST',{email,password:fixture.password})).status,200);
|
||||
},
|
||||
async select(company=fixture.company,unit=fixture.unit) {
|
||||
const result=await this.request('/api/context','POST',{tenant_id:fixture.tenant,company_id:company,unit_id:unit});
|
||||
assert.equal(result.status,200); return result.data.context.token;
|
||||
}
|
||||
};
|
||||
}
|
||||
const manager=client(); await manager.login('gestor@example.test'); const token=await manager.select();
|
||||
assert.equal((await manager.request(`/api/companies/${fixture.company}`,'PATCH',{name:'CSRF must refuse'},{csrfHeader:false})).status,419);
|
||||
results.push('CSRF: pedido de alteração sem token/origem válida recusado (419).');
|
||||
const reader=client(); await reader.login('consulta@example.test'); const readerToken=await reader.select();
|
||||
assert.equal((await reader.request(`/api/companies/${fixture.sibling}`)).status,403);
|
||||
assert.equal((await reader.request(`/api/units/${fixture.hidden}`)).status,403);
|
||||
assert.equal((await reader.request(`/api/companies/${fixture.company}`,'PATCH',{name:'Not allowed'})).status,403);
|
||||
results.push('URLs/API: empresa do mesmo tenant e unidade sem acesso recusadas; consulta não permite edição (403).');
|
||||
const grant={email:'consulta@example.test',permission:'company.edit',reason:'Prova HTTP sintética'};
|
||||
assert.equal((await manager.request('/api/accesses','POST',grant)).status,200);
|
||||
assert.equal((await reader.request(`/api/companies/${fixture.company}`,'PATCH',{name:'Empresa Horizonte — Prova HTTP'})).status,200);
|
||||
assert.equal((await manager.request('/api/accesses/revoke','POST',grant)).status,200);
|
||||
assert.equal((await reader.request(`/api/companies/${fixture.company}`,'PATCH',{name:'Revoked'})).status,403);
|
||||
results.push('Concessão e revogação: efeito no pedido seguinte em sessão independente (200 → 403).');
|
||||
await manager.select(fixture.sibling,null);
|
||||
assert.equal((await manager.request(`/api/companies/${fixture.company}`,'PATCH',{name:'Stale'},{token})).status,409);
|
||||
results.push('Formulário anterior à mudança de contexto recusado (409).');
|
||||
const limited=client(); await limited.login('limitado@example.test'); await limited.select();
|
||||
assert.equal((await limited.request('/api/accesses','POST',grant)).status,403);
|
||||
results.push('Gestor limitado não consegue conceder edição que não possui (403).');
|
||||
const oldCookies=new Map(reader.cookies);
|
||||
assert.equal((await reader.request('/api/logout','POST')).status,200);
|
||||
reader.cookies.clear(); for(const [key,value] of oldCookies) reader.cookies.set(key,value);
|
||||
assert.equal((await reader.request('/api/context')).status,401);
|
||||
results.push('Replay do cookie da sessão terminada recusado (401).');
|
||||
await manager.request('/api/logout','POST'); await limited.request('/api/logout','POST');
|
||||
await fs.writeFile(new URL('../docs/proofs/http-results.json',import.meta.url),JSON.stringify({environment:'sgf_tempo_test, browser-testing, localhost:8001',results},null,2));
|
||||
console.log(results.join('\n'));
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$db = \Illuminate\Support\Facades\DB::selectOne('SELECT current_database() AS db, current_user AS role');
|
||||
$counts = [];
|
||||
foreach (['users', 'tenants', 'companies', 'units', 'company_memberships', 'permission_grants', 'identity_audit'] as $table) {
|
||||
$counts[$table] = \Illuminate\Support\Facades\Schema::hasTable($table) ? \Illuminate\Support\Facades\DB::table($table)->count() : 'pending migration';
|
||||
}
|
||||
echo json_encode(['database' => $db->db, 'role' => $db->role, 'counts' => $counts], JSON_PRETTY_PRINT).PHP_EOL;
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
foreach (['users', 'tenants', 'companies', 'units'] as $table) {
|
||||
if (\Illuminate\Support\Facades\DB::table($table)->exists()) {
|
||||
fwrite(STDERR, "Test database is not empty. No data was deleted. Stop other test workflows and prepare the test database explicitly.\n"); exit(1);
|
||||
}
|
||||
}
|
||||
$fixture = \Illuminate\Support\Facades\DB::transaction(fn () => \Tests\Support\IdentityFixture::create());
|
||||
$safe = array_intersect_key($fixture, array_flip(['tenant', 'otherTenant', 'company', 'sibling', 'foreign', 'unit', 'hidden', 'siblingUnit', 'password']));
|
||||
file_put_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json', json_encode($safe, JSON_THROW_ON_ERROR));
|
||||
echo "Synthetic browser fixtures prepared only in sgf_tempo_test. Credentials stored locally with restricted ACL; not displayed.\n";
|
||||
@@ -0,0 +1,44 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Email,
|
||||
[Parameter(Mandatory=$true)][string]$Name,
|
||||
[Parameter(Mandatory=$true)][string]$Operator,
|
||||
[Parameter(Mandatory=$true)][string]$Reason
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$project = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
if ($project -ne 'C:\laragon\www\sgf-tempo') { throw 'Unexpected project path.' }
|
||||
Set-Location -LiteralPath $project
|
||||
$OutputEncoding = New-Object Text.UTF8Encoding $false
|
||||
[Console]::OutputEncoding = $OutputEncoding
|
||||
$sgfPhp = 'C:\laragon\tools\sgf-tempo\php-8.4.27\php.exe'
|
||||
$arguments = @('artisan','identity:provision',"--email=$Email","--name=$Name","--operator=$Operator","--reason=$Reason")
|
||||
& $sgfPhp @arguments
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Preview refused. No account created.' }
|
||||
$answer = Read-Host 'Confirma criar apenas esta conta, sem permissoes automaticas? [sim/nao]'
|
||||
if ($answer -notin @('sim','s','yes','y')) { Write-Host 'Operacao cancelada. Nada criado.'; exit 1 }
|
||||
Write-Host 'Defina a palavra-passe apenas neste terminal. Minimo 12 caracteres; maximo 72 bytes UTF-8.'
|
||||
while ($true) {
|
||||
$first = Read-Host 'Nova palavra-passe' -AsSecureString
|
||||
$repeat = Read-Host 'Repita a palavra-passe' -AsSecureString
|
||||
$firstPtr = [IntPtr]::Zero; $repeatPtr = [IntPtr]::Zero
|
||||
$plainFirst = $null; $plainRepeat = $null; $payload = $null
|
||||
try {
|
||||
$firstPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($first)
|
||||
$repeatPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($repeat)
|
||||
$plainFirst = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($firstPtr)
|
||||
$plainRepeat = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($repeatPtr)
|
||||
if ($plainFirst.Length -lt 12) { Write-Host 'Use pelo menos 12 caracteres. Tente novamente.' -ForegroundColor Yellow; continue }
|
||||
if ([Text.Encoding]::UTF8.GetByteCount($plainFirst) -gt 72) { Write-Host 'Excede 72 bytes. Tente novamente.' -ForegroundColor Yellow; continue }
|
||||
if ($plainFirst -cne $plainRepeat) { Write-Host 'As entradas nao coincidem. Tente novamente.' -ForegroundColor Yellow; continue }
|
||||
$payload = @{confirmed=$true;email=$Email.Trim().ToLowerInvariant();password=$plainFirst;confirmation=$plainRepeat} | ConvertTo-Json -Compress
|
||||
$payload | & $sgfPhp @arguments --apply --protected-stdin
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Account creation refused. Copy only the error message, never the password.' }
|
||||
Write-Host 'Conta criada sem acessos automaticos. Um gestor autorizado pode agora conceder os direitos na aplicacao.' -ForegroundColor Green
|
||||
break
|
||||
} finally {
|
||||
if ($firstPtr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($firstPtr) }
|
||||
if ($repeatPtr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($repeatPtr) }
|
||||
$plainFirst=$null; $plainRepeat=$null; $payload=$null
|
||||
$first.Dispose(); $repeat.Dispose()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
$public = realpath(__DIR__.'/../public');
|
||||
$uri = rawurldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/');
|
||||
$file = realpath($public.$uri);
|
||||
if ($uri !== '/' && $file && str_starts_with($file, $public.DIRECTORY_SEPARATOR) && is_file($file)) {
|
||||
return false;
|
||||
}
|
||||
require $public.'/index.php';
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
param(
|
||||
[ValidateSet('start', 'stop', 'status', 'frontend', 'php', 'composer', 'npm', 'test', 'migrate')]
|
||||
[string]$Action = 'status',
|
||||
[Parameter(ValueFromRemainingArguments = $true)] [string[]]$ToolArguments
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$sgfProject = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
if ($sgfProject -ne 'C:\laragon\www\sgf-tempo') { throw 'Unexpected project path.' }
|
||||
$sgfTools = 'C:\laragon\tools\sgf-tempo'
|
||||
$sgfData = 'C:\laragon\data\sgf-tempo'
|
||||
$sgfCluster = Join-Path $sgfData 'postgres-17'
|
||||
$sgfPg = 'C:\Program Files\PostgreSQL\17\bin'
|
||||
$sgfPhp = "$sgfTools\php-8.4.27\php.exe"
|
||||
$sgfNode = "$sgfTools\node-v24.22.1-win-arm64\node.exe"
|
||||
$sgfComposer = "$sgfTools\composer-2.10.3\composer.phar"
|
||||
$sgfNpmCli = "$sgfTools\node-v24.22.1-win-arm64\node_modules\npm\bin\npm-cli.js"
|
||||
$sgfRun = Join-Path $sgfData 'run'
|
||||
$env:PATH = "$sgfTools\php-8.4.27;$sgfTools\node-v24.22.1-win-arm64;" + $env:PATH
|
||||
$env:COMPOSER_HOME = "$sgfTools\composer-home"
|
||||
$env:COMPOSER_CACHE_DIR = "$sgfTools\composer-cache"
|
||||
$env:npm_config_cache = "$sgfTools\npm-cache"
|
||||
$env:npm_config_userconfig = "$sgfTools\npmrc"
|
||||
Remove-Item Env:COMPOSER_AUTH -ErrorAction SilentlyContinue
|
||||
Set-Location -LiteralPath $sgfProject
|
||||
New-Item -ItemType Directory -Path $sgfRun -Force | Out-Null
|
||||
|
||||
function Assert-FreePort([int]$Port) {
|
||||
if (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) {
|
||||
throw "Port $Port is occupied. No process was stopped."
|
||||
}
|
||||
}
|
||||
function Start-SgfDatabase {
|
||||
$listeners = @(Get-NetTCPConnection -LocalPort 55432 -State Listen -ErrorAction SilentlyContinue)
|
||||
if ($listeners.Count) {
|
||||
$pidFile = Join-Path $sgfCluster 'postmaster.pid'
|
||||
if (!(Test-Path -LiteralPath $pidFile)) { throw 'Port 55432 belongs to another process.' }
|
||||
$clusterPid = [int](Get-Content -LiteralPath $pidFile -TotalCount 1)
|
||||
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $clusterPid"
|
||||
$normalizedCommand = if ($process) { $process.CommandLine.Replace('/', '\') } else { '' }
|
||||
if (($listeners.OwningProcess -notcontains $clusterPid) -or !$process -or $process.ExecutablePath -ne "$sgfPg\postgres.exe" -or $normalizedCommand -notlike "*$sgfCluster*") {
|
||||
throw 'Port 55432 is not owned by the SGF cluster. No process was stopped.'
|
||||
}
|
||||
return
|
||||
}
|
||||
Assert-FreePort 55432
|
||||
& "$sgfPg\pg_ctl.exe" -D $sgfCluster -l (Join-Path $sgfData 'postgres.log') -w start
|
||||
if ($LASTEXITCODE) { throw 'SGF database start failed.' }
|
||||
}
|
||||
function Start-SgfProcess([string]$Name, [string]$Executable, [string[]]$Arguments, [int]$Port) {
|
||||
Assert-FreePort $Port
|
||||
$process = Start-Process -FilePath $Executable -ArgumentList $Arguments -WorkingDirectory $sgfProject -WindowStyle Hidden -PassThru -RedirectStandardOutput "$sgfRun\$Name.stdout.log" -RedirectStandardError "$sgfRun\$Name.stderr.log"
|
||||
$record = @{pid=$process.Id; executable=$Executable; project=$sgfProject; port=$Port; started=$process.StartTime.ToUniversalTime().ToString('o')}
|
||||
$record | ConvertTo-Json | Set-Content -LiteralPath "$sgfRun\$Name.json" -Encoding UTF8
|
||||
$ready = $false
|
||||
for ($attempt = 0; $attempt -lt 40; $attempt++) {
|
||||
$process.Refresh()
|
||||
if ($process.HasExited) { throw "$Name exited. See SGF run logs." }
|
||||
$listeners = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue)
|
||||
if ($listeners.OwningProcess -contains $process.Id) { $ready = $true; break }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
if (!$ready) { throw "$Name did not become ready. No other process was stopped." }
|
||||
Write-Output "$Name ready at http://127.0.0.1:$Port"
|
||||
}
|
||||
|
||||
switch ($Action) {
|
||||
'start' {
|
||||
Assert-FreePort 8000
|
||||
Start-SgfDatabase
|
||||
Start-SgfProcess 'web' $sgfPhp @('-S', '127.0.0.1:8000', '-t', 'public', 'scripts/router.php') 8000
|
||||
}
|
||||
'frontend' {
|
||||
Start-SgfProcess 'vite' $sgfNode @('node_modules/vite/bin/vite.js', '--host', '127.0.0.1', '--port', '5173', '--strictPort') 5173
|
||||
}
|
||||
'status' {
|
||||
& "$sgfPg\pg_ctl.exe" -D $sgfCluster status
|
||||
Get-NetTCPConnection -LocalPort 55432,8000,5173 -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress,LocalPort,OwningProcess
|
||||
}
|
||||
'stop' {
|
||||
foreach ($name in @('vite', 'web')) {
|
||||
$recordFile = "$sgfRun\$name.json"
|
||||
if (Test-Path -LiteralPath $recordFile) {
|
||||
$record = Get-Content -LiteralPath $recordFile -Raw | ConvertFrom-Json
|
||||
$process = Get-Process -Id $record.pid -ErrorAction SilentlyContinue
|
||||
if ($process) {
|
||||
if ($process.Path -ne $record.executable -or $record.project -ne $sgfProject -or $process.StartTime.ToUniversalTime().ToString('o') -ne $record.started) {
|
||||
throw "Process identity mismatch for $name; refusing to stop it."
|
||||
}
|
||||
Stop-Process -Id $process.Id
|
||||
}
|
||||
Remove-Item -LiteralPath $recordFile
|
||||
if ($name -eq 'vite') {
|
||||
$hotFile = Join-Path $sgfProject 'public\hot'
|
||||
if ((Test-Path -LiteralPath $hotFile) -and (Get-Content -LiteralPath $hotFile -Raw).Trim() -eq 'http://127.0.0.1:5173') {
|
||||
Remove-Item -LiteralPath $hotFile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
& "$sgfPg\pg_ctl.exe" -D $sgfCluster -m fast -w stop
|
||||
if ($LASTEXITCODE -ne 0 -and (Test-Path -LiteralPath "$sgfCluster\postmaster.pid")) { throw 'SGF cluster stop failed.' }
|
||||
}
|
||||
'php' { & $sgfPhp @ToolArguments; if ($LASTEXITCODE) { throw 'PHP command failed.' } }
|
||||
'composer' { & $sgfPhp $sgfComposer @ToolArguments; if ($LASTEXITCODE) { throw 'Composer command failed.' } }
|
||||
'npm' { & $sgfNode $sgfNpmCli @ToolArguments; if ($LASTEXITCODE) { throw 'npm command failed.' } }
|
||||
'test' {
|
||||
if (Test-Path -LiteralPath "$sgfRun\browser-proof.json") { throw 'Stop the browser proof before tests reset sgf_tempo_test.' }
|
||||
Start-SgfDatabase
|
||||
& $sgfPhp 'vendor/phpunit/phpunit/phpunit' @ToolArguments
|
||||
if ($LASTEXITCODE) { throw 'Tests failed.' }
|
||||
}
|
||||
'migrate' {
|
||||
Start-SgfDatabase
|
||||
$credentials = Get-Content -LiteralPath "$sgfData\secrets\database.json" -Raw | ConvertFrom-Json
|
||||
$env:APP_ENV = 'local'
|
||||
$env:DB_CONNECTION = 'pgsql'
|
||||
$env:DB_HOST = '127.0.0.1'
|
||||
$env:DB_PORT = '55432'
|
||||
$env:DB_DATABASE = 'sgf_tempo_dev'
|
||||
$env:DB_USERNAME = 'sgf_dev_owner'
|
||||
$env:DB_PASSWORD = $credentials.dev_owner
|
||||
$env:DB_URL = ''
|
||||
try {
|
||||
& $sgfPhp artisan migrate --no-interaction
|
||||
if ($LASTEXITCODE) { throw 'Development migration failed.' }
|
||||
} finally {
|
||||
foreach ($name in @('APP_ENV','DB_CONNECTION','DB_HOST','DB_PORT','DB_DATABASE','DB_USERNAME','DB_PASSWORD','DB_URL')) { Remove-Item "Env:$name" -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
$identity = DB::selectOne('SELECT current_database() AS db, current_user AS role');
|
||||
$privileges = DB::selectOne("SELECT rolsuper, rolcreatedb, rolcreaterole, rolbypassrls FROM pg_roles WHERE rolname = current_user");
|
||||
$ddl = DB::selectOne("SELECT has_schema_privilege(current_user, 'public', 'CREATE') AS allowed");
|
||||
$crossDatabase = DB::selectOne("SELECT has_database_privilege(current_user, 'sgf_tempo_test', 'CONNECT') AS allowed");
|
||||
if ($identity->db !== 'sgf_tempo_dev' || $identity->role !== 'sgf_dev_app'
|
||||
|| $privileges->rolsuper || $privileges->rolcreatedb || $privileges->rolcreaterole
|
||||
|| $privileges->rolbypassrls || $ddl->allowed || $crossDatabase->allowed) {
|
||||
fwrite(STDERR, "Development database identity/privilege check failed.\n");
|
||||
exit(1);
|
||||
}
|
||||
if (DB::table('users')->where('email', 'like', '%@example.test')->exists()) {
|
||||
fwrite(STDERR, "Synthetic test accounts must not be present in development.\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "Development connection: sgf_dev_app; no administration, schema creation or test database access. No test fixtures in development.\n";
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
try {
|
||||
[$password, $confirmation] = \App\Modules\Identity\Services\ProtectedProvisioningInput::read(STDIN, 'synthetic@example.test');
|
||||
$expected = getenv('SGF_SYNTHETIC_HASH');
|
||||
if (! is_string($expected) || $expected === '' || ! hash_equals($expected, hash('sha256', $password)) || $password !== $confirmation) { exit(1); }
|
||||
echo "Protected PowerShell-to-PHP transport: OK (synthetic input, no database writes).\n";
|
||||
} catch (\Throwable) { fwrite(STDERR, "Protected transport verification failed; payload not displayed.\n"); exit(1); }
|
||||
Reference in New Issue
Block a user