commit12
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Shared\Context;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class ModuleNavigation
|
||||
{
|
||||
public const LABELS = ['commercial'=>'Comercial', 'purchasing'=>'Compras', 'stock'=>'Stock', 'workshop'=>'Oficinas', 'finance'=>'Financeiro', 'accounting'=>'Contabilidade'];
|
||||
private const RIGHTS = [
|
||||
'commercial'=>['clients.', 'services.', 'service_drafts.', 'billing_drafts.', 'issuer_documents.', 'client_documents.', 'draft_documents.', 'service_invoices.'],
|
||||
'purchasing'=>['purchasing_budget.', 'supplier_payables.'],
|
||||
'stock'=>['stock.'], 'workshop'=>['workshop.', 'workshop_billing.', 'workshop_corrections.'],
|
||||
'finance'=>['direct_costs.', 'industrial_costs.', 'industrial_budgets.', 'industrial_budget_comparisons.', 'service_receivables.'],
|
||||
'accounting'=>['catalogue_accounting.', 'service_fiscal.'],
|
||||
];
|
||||
|
||||
public function available(CompanyContext $context): array
|
||||
{
|
||||
$s = $context->identifiers();
|
||||
$grants = DB::table('permission_grants')->where($s)->where('user_id', $context->userId())
|
||||
->where(fn($q) => $q->whereNull('unit_id')->orWhere('unit_id', $context->unitId()))->pluck('permission')->all();
|
||||
$development = app()->environment(['local','testing','browser-testing'])
|
||||
&& in_array($context->userId().':'.$s['tenant_id'].':'.$s['company_id'], config('modules.development_profiles', []), true);
|
||||
if ($development) {
|
||||
// A local environment name alone must never turn client accounts into
|
||||
// development profiles. Discovery also requires the isolated DB guard.
|
||||
try { \App\Shared\Testing\TestingDatabaseGuard::assertSafe(); }
|
||||
catch (\RuntimeException) { $development=false; }
|
||||
}
|
||||
$policy = config('modules.companies.'.$s['tenant_id'].':'.$s['company_id'], []);
|
||||
$contracted = $policy['contracted'] ?? config('modules.contracted', []);
|
||||
$enabled = $policy['enabled'] ?? $contracted;
|
||||
$out = [];
|
||||
foreach (self::LABELS as $id => $label) {
|
||||
$authorized = false;
|
||||
foreach ($grants as $permission) {
|
||||
foreach (self::RIGHTS[$id] as $prefix) {
|
||||
if (str_starts_with($permission, $prefix) && str_ends_with($permission, '.view')) { $authorized = true; }
|
||||
}
|
||||
}
|
||||
if ($development || ($authorized && in_array($id,$contracted,true) && in_array($id,$enabled,true))) {
|
||||
$out[] = ['id'=>$id, 'label'=>$label, 'development'=>$development, 'operations_authorized'=>$authorized];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
// Ownership of existing endpoints, not an attribution of legacy rows to a new module.
|
||||
// Company, unit and access configuration are explicitly general.
|
||||
public static function owner(string $path): ?string
|
||||
{
|
||||
if (preg_match('~^(supplier-payables|supplier-payments|api/(supplier-payables|supplier-payments|purchasing-budget))(/|$)~', $path)) return 'purchasing';
|
||||
if (preg_match('~^api/(workshop|workshop-billing|workshop-corrections)(/|$)~', $path)) return 'workshop';
|
||||
if (preg_match('~^api/(direct-costs|industrial-costs|industrial-budgets|service-receivables)(/|$)~', $path)) return 'finance';
|
||||
if (preg_match('~^api/(catalogue-accounting|service-fiscal)(/|$)~', $path)) return 'accounting';
|
||||
if (preg_match('~^api/(a|b1|service-invoices|service-calculations|document-preview|document-preparation)(/|$)~', $path)) return 'commercial';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Shared\Context;
|
||||
|
||||
use App\Modules\Identity\Services\Audit;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** Explicit, append-only associations reuse the existing immutable audit store.
|
||||
* Legacy rows are never assigned from their names, creators or absence of a link.
|
||||
*/
|
||||
final class ModuleRecords
|
||||
{
|
||||
public const AREAS = ['clients','services','service_drafts','billing_drafts'];
|
||||
public static function active(): ?string { return request()->attributes->get('sgf_module'); }
|
||||
public static function query(string $table)
|
||||
{
|
||||
$query=DB::table($table);
|
||||
$module=self::active();
|
||||
if (!$module) return $query; // CLI domain proofs have no HTTP module selection.
|
||||
if (str_starts_with($table,'a_') && in_array(substr($table,2),self::AREAS,true)) {
|
||||
self::constrain($query,$table,substr($table,2),$module);
|
||||
} elseif ($table==='si_invoices') {
|
||||
$query->whereIn('draft_id',self::query('a_billing_drafts')->select('id'));
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
public static function constrain($query,string $table,string $area,string $module): void
|
||||
{
|
||||
$query->whereExists(function($q)use($table,$area,$module){
|
||||
$q->selectRaw('1')->from('identity_audit as ma')->where('ma.action','module.record.associated')
|
||||
->whereColumn('ma.tenant_id',$table.'.tenant_id')->whereColumn('ma.company_id',$table.'.company_id')
|
||||
->whereRaw("ma.details::jsonb->>'area' = ?",[$area])
|
||||
->whereRaw("ma.details::jsonb->>'record_id' = {$table}.id::text")
|
||||
->whereRaw("ma.details::jsonb->>'module_id' = ?",[$module])
|
||||
->whereNotExists(function($revoked){
|
||||
$revoked->selectRaw('1')->from('identity_audit as mr')->where('mr.action','module.record.disassociated')
|
||||
->whereColumn('mr.tenant_id','ma.tenant_id')->whereColumn('mr.company_id','ma.company_id')->whereColumn('mr.id','>','ma.id')
|
||||
->whereRaw("mr.details::jsonb->>'area' = ma.details::jsonb->>'area'")
|
||||
->whereRaw("mr.details::jsonb->>'record_id' = ma.details::jsonb->>'record_id'")
|
||||
->whereRaw("mr.details::jsonb->>'module_id' = ma.details::jsonb->>'module_id'");
|
||||
});
|
||||
});
|
||||
}
|
||||
public static function associate(string $area,int $id,int $actor,array $scope,string $module,string $reason,bool $remove=false,?int $version=null): void
|
||||
{
|
||||
Audit::record($remove?'module.record.disassociated':'module.record.associated',$actor,$scope['tenant_id'],$scope['company_id'],$scope['unit_id']??null,
|
||||
details:['area'=>$area,'record_id'=>$id,'module_id'=>$module,'reason'=>$reason,'version'=>$version]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Shared\Context;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class RequireModuleContext
|
||||
{
|
||||
public function check(Request $request, CompanyContext $context): void
|
||||
{
|
||||
$owner = ModuleNavigation::owner($request->path());
|
||||
if ($owner === null) return;
|
||||
$selected = $request->session()->get('company_context', []);
|
||||
$active = $selected['module_id'] ?? null;
|
||||
abort_unless($active, 409, 'Seleccione explicitamente um módulo antes de abrir este percurso.');
|
||||
$available = array_column(app(ModuleNavigation::class)->available($context), 'id');
|
||||
abort_unless(in_array($active, $available, true), 403, 'O acesso ao módulo foi revogado ou a configuração mudou.');
|
||||
$shared = $request->is('api/a/*','api/b1/*','api/document-preview/draft/*','api/document-preview/history/*','api/document-preparation/*');
|
||||
abort_unless($active === $owner || $shared, 409, 'Este percurso pertence a outro módulo. Mude de módulo antes de o abrir.');
|
||||
$request->attributes->set('sgf_module',$active);
|
||||
// Reads also carry the generation: stale tabs and late requests cannot mix contexts.
|
||||
// Standalone HTML pages use the current session; their APIs must carry the token.
|
||||
if ($request->is('api/*') && !$request->is('api/service-invoices/*/pdf')) {
|
||||
abort_unless(hash_equals($selected['token'] ?? '', (string)$request->header('X-SGF-Context')), 409, 'O contexto mudou. Reabra este percurso.');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user