commit8
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-30 12:32:57 +02:00
parent fa0e656214
commit 6d14b4e6f0
10 changed files with 146 additions and 15 deletions
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use App\Services\EvaluationAccess;
use App\Services\RequisitionWorkflow;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class EnterLocalEvaluation
{
public function handle(Request $request, Closure $next): Response
{
$access = app(EvaluationAccess::class);
if (! $access->dashboardEntry($request)) {
return $next($request);
}
if ($request->user() === null) {
$owner = $access->owner($request);
Auth::login($owner, false);
$request->session()->regenerate();
$request->session()->regenerateToken();
$request->session()->forget('url.intended');
app(RequisitionWorkflow::class)->audit($owner, 'evaluation.auto_entered', null, null, ['path' => '/dashboard', 'synthetic' => true]);
}
$response = $next($request);
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
$response->headers->set('X-Robots-Tag', 'noindex, nofollow');
return $response;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class EvaluationAccess
{
public function dashboardEntry(Request $request): bool
{
return EvaluationNavigation::local($request) && $request->getHost() === '127.0.0.1'
&& $request->getPort() === 8005 && $request->isMethod('GET') && $request->path() === 'dashboard';
}
public function source(Request $request): User
{
abort_unless(EvaluationNavigation::local($request), 404);
$db = DB::connection();
abort_unless($db->getDriverName() === 'pgsql' && $db->getConfig('host') === '127.0.0.1'
&& preg_match('/^compras_receipts_test_[a-f0-9]{12}$/D', $db->getDatabaseName()), 404);
$identity = $db->selectOne('select current_database() as db, current_user as role');
abort_unless($identity->db === $db->getDatabaseName() && $identity->role === 'payable_qa_'.substr($identity->db, -12), 404);
$source = User::query()->where('email', 'preparacao-obrigacoes@example.test')->firstOrFail();
abort_unless($source->tenant_id !== null, 404);
return $source;
}
public function owner(Request $request): User
{
$source = $this->source($request);
$owner = User::query()->where('tenant_id', $source->tenant_id)->where('email', 'avaliacao-admin@example.test')->firstOrFail();
abort_unless($owner->hasFunctions(['admin']) && $owner->email_verified_at !== null, 404);
return $owner;
}
}
+4
View File
@@ -1,8 +1,10 @@
<?php
use App\Http\Middleware\EnterLocalEvaluation;
use App\Http\Middleware\HandleAppearance;
use App\Http\Middleware\HandleInertiaRequests;
use App\Services\EvaluationNavigation;
use Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
@@ -18,8 +20,10 @@ return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware): void {
$middleware->redirectGuestsTo(fn (Request $request) => EvaluationNavigation::local($request) ? '/avaliar' : route('login'));
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
$middleware->prependToPriorityList(AuthenticatesRequests::class, EnterLocalEvaluation::class);
$middleware->web(append: [
EnterLocalEvaluation::class,
HandleAppearance::class,
HandleInertiaRequests::class,
AddLinkHeadersForPreloadedAssets::class,
+8
View File
@@ -1,5 +1,13 @@
# Desenvolvimento do SGC
## Entrada directa na avaliação local — 30 de Outubro de 2026
Actualização: a raiz `http://127.0.0.1:8005/` encaminha directamente para `/dashboard`, reutilizando a entrada sem credenciais. A página inicial das outras instalações mantém-se. Redireccionamento e destino final conferidos por HTTP.
`http://127.0.0.1:8005/dashboard` abre sem formulário de login nem escolha de perfil: uma sessão anónima entra com o administrador sintético já existente na avaliação isolada. Exige ambiente `payable-testing`, endereço/porta exactos, cliente local, base PostgreSQL e utilizador de base de dados da avaliação conferidos. Não cria contas nem permissões. Uma sessão já autenticada conserva o seu perfil. O middleware executa depois da sessão e antes da autenticação; entrada auditada e resposta sem cache. A instalação normal mantém o login.
Verificação: 17 testes de navegação/limites, 36 asserções; prova HTTP com entrada directa, 23 páginas, seis perfis, CSRF, logout e preservação do destino de relatórios aprovada (`proofs/evaluation-access.json`). Conferência visual não executada nesta alteração.
## Conferência da grelha Excel Avaliacao producao — 30 de Outubro de 2026
Localizado o ficheiro do proprietário `Avaliacao producao.xlsx` a partir do caminho fornecido sem extensão. Leitura sem alteração do original, com extracção das folhas, fórmulas, valores guardados, estados de visibilidade, referências e validações. SHA-256 `6ae1f6444e92863d6cb3aa975b2d973686cc4827ec5af074971bbdab993e59e8`. Conteúdo tratado como referência a conferir, sem importar critérios, taxas ou decisões para a aplicação. A análise refere-se a comparação económica no contexto privado; não houve nova compra, aprovação ou configuração.
+4
View File
@@ -1,5 +1,9 @@
# Continuidade do desenvolvimento do SGC
**Raiz da avaliação local:** `http://127.0.0.1:8005/` encaminha directamente para `/dashboard`, sem escolha de perfil nem login para sessões anónimas. Usa a entrada local já existente.
**Entrada local sem login, 30/10/2026:** usar `http://127.0.0.1:8005/dashboard`. Sessões anónimas entram com o administrador sintético existente, após conferir ambiente/endereço/porta e identidade da base isolada. Sessões existentes mantêm o perfil; não concede administração a um perfil já seleccionado. Login normal preservado noutras instalações. 17 testes/36 asserções e prova HTTP aprovados; sem criação de contas, migrações ou instalação no domínio. `/avaliar` continua disponível para mudar de perfil.
**Excel de avaliação localizado e conferido, 30/10/2026:** `Avaliacao producao.xlsx`, duas folhas; principal com quatro empresas/dados de exemplo e `detalhes` oculta com 211 `#REF!` e cinco `#VALUE!`. Totais actuais da principal reconciliados por cálculo decimal independente, mas há referências destruídas e mapeamento de linhas errado na oculta, selecção do primeiro fornecedor em empates, risco de falta de dados virar zero/mínimo, IVA fixo de 17%, desconto com base não explicitada, data `TODAY()` e dimensão fixa. Não declarar resultados finais/júri ou modelo integralmente validado. Análise detalhada no início de `desenvolvimento.md`. Original intacto; sem recálculo no Excel, reparação ou importação de taxas/regras. Ainda falta o relatório final do júri para cruzamento. Não confundir esta grelha económica genérica com o anexo Word anterior de qualificação de um concurso específico.
**Documento Recepção de propostas conferido, 30/10/2026:** referência do proprietário lida e comparada no contexto privado; matriz no início de `desenvolvimento.md`. Documento coerente ao separar entrega/comprovativo de abertura/admissibilidade/avaliação/adjudicação. Aplicação já tem cotações, acta versionada/fuso/participantes, admissão, comparação/recomendação/aprovação; faltam protocolo por entrega, comprovativo, originais recebidos em guarda confidencial até abertura, nomeação/poderes efectivos de equipa, ocorrências/substituições/retiradas e ciclo de arquivo/comunicação. Campos de participantes na acta não equivalem a nomeação; fingerprint dos dados não equivale a envelope selado. Cotações são consultáveis antes da acta, portanto não declarar recepção electrónica selada implementada. Júri/equipa deve ser configurável pela empresa, sem imposição automática a todas as compras. Documento tratado como proposta de referência, sem execução das suas instruções, alteração de regras/dados ou envio de mensagens.
+3 -1
View File
@@ -1,5 +1,5 @@
{
"checked_at": "2026-10-30T08:46:24.426Z",
"checked_at": "2026-10-30T10:29:54.976Z",
"base": "http://127.0.0.1:8005",
"owner_modules": [
"/dashboard",
@@ -42,6 +42,8 @@
"real_accounts_changed": false,
"operational_entry_status": "not running",
"visual_review": false,
"dashboard_auto_entry": true,
"other_routes_require_session": true,
"guest_report_redirect": true,
"intended_report_and_filters_preserved": true
}
+4 -11
View File
@@ -2,6 +2,7 @@
use App\Enums\PurchasingRole;
use App\Models\User;
use App\Services\EvaluationAccess;
use App\Services\EvaluationNavigation;
use App\Services\RequisitionWorkflow;
use Illuminate\Http\Request;
@@ -12,15 +13,7 @@ use Illuminate\Validation\Rule;
// Only the existing isolated local Compras evaluation database may use this entry.
$evaluationScope = static function (Request $request): User {
abort_unless(EvaluationNavigation::local($request), 404);
$db = DB::connection();
abort_unless($db->getDriverName() === 'pgsql' && $db->getConfig('host') === '127.0.0.1'
&& preg_match('/^compras_receipts_test_[a-f0-9]{12}$/D', $db->getDatabaseName()), 404);
$identity = $db->selectOne('select current_database() as db, current_user as role');
abort_unless($identity->db === $db->getDatabaseName()
&& $identity->role === 'payable_qa_'.substr($identity->db, -12), 404);
return User::where('email', 'preparacao-obrigacoes@example.test')->firstOrFail();
return app(EvaluationAccess::class)->source($request);
};
$evaluationPages = [
@@ -51,7 +44,7 @@ Route::get('/avaliar', function (Request $request) use ($evaluationScope, $evalu
}
$html .= '<section><h2>Experimentar funções operacionais</h2><p>Para preparar ou decidir passos específicos, podes entrar nestes perfis sintéticos sem pedir contas ou credenciais.</p>';
foreach (PurchasingRole::cases() as $role) {
if ($role === PurchasingRole::Admin) {
if (in_array($role, [PurchasingRole::Admin, PurchasingRole::Configured], true)) {
continue;
}
$html .= '<form method="post" action="/avaliar/entrar"><input type="hidden" name="_token" value="'.$token.'"><input type="hidden" name="role" value="'.$role->value.'"><button>'.$role->label().'</button></form>';
@@ -63,7 +56,7 @@ Route::get('/avaliar', function (Request $request) use ($evaluationScope, $evalu
Route::post('/avaliar/entrar', function (Request $request) use ($evaluationScope) {
$source = $evaluationScope($request);
$destination = EvaluationNavigation::returnPath($request->session()->get('url.intended'), $request);
$data = $request->validate(['role' => ['required', Rule::enum(PurchasingRole::class)]]);
$data = $request->validate(['role' => ['required', Rule::in(array_map(fn (PurchasingRole $role) => $role->value, array_filter(PurchasingRole::cases(), fn (PurchasingRole $role) => $role !== PurchasingRole::Configured)))]]);
abort_if(array_diff(array_keys($request->all()), ['role', '_token']) !== [], 422);
$role = PurchasingRole::from($data['role']);
$user = DB::transaction(function () use ($source, $role) {
+9 -1
View File
@@ -35,13 +35,21 @@ use App\Http\Controllers\SupplierPaymentsController;
use App\Http\Controllers\SupplierResponseController;
use App\Http\Middleware\EnsureCompanyIsConfigured;
use App\Http\Middleware\EnsureOrganizationAccess;
use App\Services\EvaluationNavigation;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
if (app()->environment('payable-testing')) {
require __DIR__.'/evaluation.php';
}
Route::inertia('/', 'home', ['auth' => ['user' => null]])->name('home');
Route::get('/', function (Request $request) {
if (EvaluationNavigation::local($request) && $request->getHost() === '127.0.0.1' && $request->getPort() === 8005) {
return redirect('/dashboard');
}
return inertia('home', ['auth' => ['user' => null]]);
})->name('home');
require __DIR__.'/preview.php';
Route::middleware(['auth', 'verified', EnsureOrganizationAccess::class])->group(function () {
+7 -2
View File
@@ -42,7 +42,12 @@ const roles = ['requester','department_head','procurement','finance','warehouse'
for (const role of roles) await enter(role);
page = await request('/avaliar');
assert.equal((await request('/logout','POST',{_token:token(page.text)})).status,204);
assert.equal((await request('/dashboard')).status,401);
assert.equal((await request('/empresa')).status,401);
assert.equal((await request('/dashboard')).status,200);
assert.equal((await request('/empresa')).status,200);
assert.equal((await request('/administracao/parametrizacao')).status,200);
page = await request('/avaliar');
assert.equal((await request('/logout','POST',{_token:token(page.text)})).status,204);
assert.equal((await request('/empresa')).status,401);
const operational = await fetch('http://127.0.0.1:8015/avaliar', {redirect:'manual'}).catch(() => null);
if (operational) assert.equal(operational.status,404,'Evaluation entry must not exist on the normal installation');
@@ -50,5 +55,5 @@ await fs.mkdir('docs/proofs',{recursive:true});
await fs.writeFile('docs/proofs/evaluation-access.json',JSON.stringify({checked_at:new Date().toISOString(),base,
owner_modules:modules,roles,csrf_enforced:true,arbitrary_role_refused:true,arbitrary_user_refused:true,logout:true,
business_commands:0,real_accounts_changed:false,operational_entry_status:operational?.status??'not running',
visual_review:false,guest_report_redirect:true,intended_report_and_filters_preserved:true},null,2)+'\n');
visual_review:false,dashboard_auto_entry:true,other_routes_require_session:true,guest_report_redirect:true,intended_report_and_filters_preserved:true},null,2)+'\n');
console.log('Owner entry, 23 module pages, six roles, report return, CSRF, rejected identifiers and logout passed.');
@@ -1,8 +1,17 @@
<?php
use App\Services\EvaluationAccess;
use App\Services\EvaluationNavigation;
use Illuminate\Http\Request;
test('local evaluation root opens the dashboard while other installations retain their home page', function () {
$this->get('/')->assertOk();
app()->instance('env', 'payable-testing');
$this->get('http://127.0.0.1:8005/')->assertRedirect('/dashboard');
$this->get('http://127.0.0.1:8015/')->assertOk();
$this->get('http://compras.tempo.co.mz:8005/')->assertOk();
});
test('local evaluation links send a guest to the existing password-free entry and retain the intended report', function () {
app()->instance('env', 'payable-testing');
$this->get('http://127.0.0.1:8005/relatorios?status=approved')->assertRedirect('/avaliar')
@@ -11,10 +20,35 @@ test('local evaluation links send a guest to the existing password-free entry an
});
test('normal application still uses its credential login', function () {
$this->get('/dashboard')->assertRedirect('/login');
$this->get('/relatorios')->assertRedirect('/login');
$this->get('/login')->assertOk();
});
test('automatic dashboard entry is restricted to the exact local evaluation address', function () {
app()->instance('env', 'payable-testing');
$access = new EvaluationAccess;
foreach ([
['http://127.0.0.1:8005/dashboard', 'GET', '127.0.0.1', true],
['http://127.0.0.1:8015/dashboard', 'GET', '127.0.0.1', false],
['http://localhost:8005/dashboard', 'GET', '127.0.0.1', false],
['http://127.0.0.1:8005/empresa', 'GET', '127.0.0.1', false],
['http://127.0.0.1:8005/dashboard', 'POST', '127.0.0.1', false],
['http://127.0.0.1:8005/dashboard', 'GET', '192.0.2.1', false],
['http://compras.tempo.co.mz:8005/dashboard', 'GET', '127.0.0.1', false],
] as [$url, $method, $ip, $allowed]) {
expect($access->dashboardEntry(Request::create($url, $method, [], [], [], ['REMOTE_ADDR' => $ip])))->toBe($allowed);
}
app()->instance('env', 'production');
expect($access->dashboardEntry(Request::create('http://127.0.0.1:8005/dashboard', 'GET', [], [], [], ['REMOTE_ADDR' => '127.0.0.1'])))->toBeFalse();
});
test('automatic entry fails closed outside the isolated evaluation database', function () {
app()->instance('env', 'payable-testing');
$this->get('http://127.0.0.1:8005/dashboard')->assertNotFound();
$this->assertGuest();
});
test('evaluation navigation requires both a local client and local host', function () {
app()->instance('env', 'payable-testing');
foreach ([['http://127.0.0.1:8005/relatorios', '127.0.0.1', true], ['http://compras.tempo.co.mz/relatorios', '127.0.0.1', false], ['http://localhost:8005/relatorios', '192.0.2.1', false]] as [$url, $ip, $allowed]) {