@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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 () {
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
Reference in New Issue
Block a user