From 6d14b4e6f0a29be1d679c38ae8107b9b044e3142 Mon Sep 17 00:00:00 2001 From: nala Date: Fri, 30 Oct 2026 12:32:57 +0200 Subject: [PATCH] commit8 --- app/Http/Middleware/EnterLocalEvaluation.php | 34 +++++++++++++++++ app/Services/EvaluationAccess.php | 39 ++++++++++++++++++++ bootstrap/app.php | 4 ++ docs/desenvolvimento.md | 8 ++++ docs/orientacoes-continuidade.md | 4 ++ docs/proofs/evaluation-access.json | 4 +- routes/evaluation.php | 15 ++------ routes/web.php | 10 ++++- scripts/evaluation-access-check.mjs | 9 ++++- tests/Feature/EvaluationNavigationTest.php | 34 +++++++++++++++++ 10 files changed, 146 insertions(+), 15 deletions(-) create mode 100644 app/Http/Middleware/EnterLocalEvaluation.php create mode 100644 app/Services/EvaluationAccess.php diff --git a/app/Http/Middleware/EnterLocalEvaluation.php b/app/Http/Middleware/EnterLocalEvaluation.php new file mode 100644 index 0000000..5120cce --- /dev/null +++ b/app/Http/Middleware/EnterLocalEvaluation.php @@ -0,0 +1,34 @@ +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; + } +} diff --git a/app/Services/EvaluationAccess.php b/app/Services/EvaluationAccess.php new file mode 100644 index 0000000..2f45493 --- /dev/null +++ b/app/Services/EvaluationAccess.php @@ -0,0 +1,39 @@ +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; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 8230a7d..2641396 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,8 +1,10 @@ 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, diff --git a/docs/desenvolvimento.md b/docs/desenvolvimento.md index 61386d6..d1f667e 100644 --- a/docs/desenvolvimento.md +++ b/docs/desenvolvimento.md @@ -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. diff --git a/docs/orientacoes-continuidade.md b/docs/orientacoes-continuidade.md index 2552aea..3f8cb03 100644 --- a/docs/orientacoes-continuidade.md +++ b/docs/orientacoes-continuidade.md @@ -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. diff --git a/docs/proofs/evaluation-access.json b/docs/proofs/evaluation-access.json index 581f476..4d10c79 100644 --- a/docs/proofs/evaluation-access.json +++ b/docs/proofs/evaluation-access.json @@ -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 } diff --git a/routes/evaluation.php b/routes/evaluation.php index db6b95c..000e81f 100644 --- a/routes/evaluation.php +++ b/routes/evaluation.php @@ -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 .= '

Experimentar funções operacionais

Para preparar ou decidir passos específicos, podes entrar nestes perfis sintéticos sem pedir contas ou credenciais.

'; foreach (PurchasingRole::cases() as $role) { - if ($role === PurchasingRole::Admin) { + if (in_array($role, [PurchasingRole::Admin, PurchasingRole::Configured], true)) { continue; } $html .= '
'; @@ -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) { diff --git a/routes/web.php b/routes/web.php index ac78a74..dec2c1c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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 () { diff --git a/scripts/evaluation-access-check.mjs b/scripts/evaluation-access-check.mjs index 1700845..c70f009 100644 --- a/scripts/evaluation-access-check.mjs +++ b/scripts/evaluation-access-check.mjs @@ -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.'); diff --git a/tests/Feature/EvaluationNavigationTest.php b/tests/Feature/EvaluationNavigationTest.php index 75085f0..f26d62c 100644 --- a/tests/Feature/EvaluationNavigationTest.php +++ b/tests/Feature/EvaluationNavigationTest.php @@ -1,8 +1,17 @@ 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]) {