@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\PortalProfileAccess;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
class AuditProfileDecision
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
if (! $request->user() || ! $access->available() || ! preg_match('#^(perfil/|perfis/|minhas-oportunidades)#', $request->path())) {
|
||||
return $next($request);
|
||||
}
|
||||
try {
|
||||
$response = $next($request);
|
||||
} catch (\Throwable $error) {
|
||||
$status = $error instanceof HttpExceptionInterface ? $error->getStatusCode() : ($error instanceof ValidationException ? 422 : 500);
|
||||
$this->record($request, $access, $status);
|
||||
throw $error;
|
||||
}
|
||||
if ($response->getStatusCode() >= 400) {
|
||||
$this->record($request, $access, $response->getStatusCode());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function record(Request $request, PortalProfileAccess $access, int $status): void
|
||||
{
|
||||
$access->audit($request->user(), 'request_denied', ['method' => $request->method(), 'path' => $request->path(), 'status' => $status]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\PortalProfileAccess;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsurePortalProfiles
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
abort_unless(config('portal.profile_access_enabled'), 404);
|
||||
abort_unless(app(PortalProfileAccess::class)->available(), 503, 'Autorização por perfis ainda não instalada.');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\InvestorPath;
|
||||
use App\Services\OpportunityManagement;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
@@ -24,6 +26,14 @@ class HandleInertiaRequests extends Middleware
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
if (app()->environment('testing') && config('portal.qa_build_directory')) {
|
||||
$manifest = public_path(config('portal.qa_build_directory').'/manifest.json');
|
||||
|
||||
$version = is_file($manifest) ? hash_file('xxh128', $manifest) : false;
|
||||
|
||||
return $version === false ? null : $version;
|
||||
}
|
||||
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
@@ -39,11 +49,18 @@ class HandleInertiaRequests extends Middleware
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'profileAccess' => app(PortalProfileAccess::class)->navigation($request->user()),
|
||||
'portalOperator' => config('portal.operator_name'),
|
||||
'educationEnabled' => (bool) config('education.enabled', false),
|
||||
'aiEnabled' => (bool) config('investmoz_ai.enabled', false),
|
||||
'existingBusinessEnabled' => (bool) config('existing-business.enabled', false),
|
||||
'investorPathEnabled' => app(InvestorPath::class)->available(),
|
||||
'opportunityManagement' => config('portal.opportunity_dashboard_access_enabled') && $request->user() && app(PortalProfileAccess::class)->legacyWriteAllowed($request->user()) ? [
|
||||
'installed' => app(OpportunityManagement::class)->installed(),
|
||||
'manage' => ['label' => 'Gestão de Oportunidades', 'href' => '/management/opportunities'],
|
||||
'create' => ['label' => 'Nova oportunidade', 'href' => '/management/opportunities/create'],
|
||||
'installationHref' => '/management/opportunity-management-install',
|
||||
] : null,
|
||||
'flash' => ['success' => fn () => $request->session()->get('success'), 'warning' => fn () => $request->session()->get('warning')],
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\PortalProfileAccess;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PortalProfileSession
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
$available = $access->available();
|
||||
if ($request->user() && config('portal.profile_access_enabled') && ! $available) {
|
||||
$recovery = $request->is('management/profile-install', 'management/profile-install/bootstrap')
|
||||
|| ($request->is('logout') && $request->isMethod('POST'));
|
||||
abort_if(! in_array($request->method(), ['GET', 'HEAD', 'OPTIONS'], true) && ! $recovery, 503,
|
||||
'Autorizações indisponíveis: recuperar a consistência dos perfis antes de gravar. Pode terminar a sessão ou consultar o instalador.');
|
||||
}
|
||||
if ($request->user() && $available) {
|
||||
$revision = (int) DB::table('users')->where('id', $request->user()->id)->value('portal_access_revision');
|
||||
$bound = $request->session()->get('portal.access_revision');
|
||||
if (($bound === null && $revision > 0) || ($bound !== null && (int) $bound !== $revision)) {
|
||||
Auth::guard('web')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return $request->expectsJson() ? response()->json(['message' => 'A autorização foi alterada. Inicie uma nova sessão.'], 401)
|
||||
: redirect('/login')->withErrors(['email' => 'A autorização foi alterada. Inicie uma nova sessão.']);
|
||||
}
|
||||
$request->session()->put('portal.access_revision', $revision);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\PortalProfileAccess;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ProtectPrivilegedIdentity
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$actor = $request->user();
|
||||
$access = app(PortalProfileAccess::class);
|
||||
if (! $actor || ! $access->available()) {
|
||||
return $next($request);
|
||||
}
|
||||
if (! in_array($request->method(), ['GET', 'HEAD', 'OPTIONS'], true) && $access->has($actor, 'auditor')
|
||||
&& $request->is('management/*', 'api/v1/investment/admin/*', 'api/v2/investment/admin/*')) {
|
||||
$access->audit($actor, 'auditor_administrative_write_denied', ['method' => $request->method(), 'path' => $request->path()]);
|
||||
abort(403, 'Auditor só permite leitura administrativa, mesmo com outro papel ou permissão legada.');
|
||||
}
|
||||
$mandatoryMfa = $access->superuser($actor) || $access->has($actor, 'validator') || $access->has($actor, 'publisher');
|
||||
$denied = $request->isMethod('DELETE') && $request->is('user/two-factor-authentication') && $mandatoryMfa;
|
||||
$denied = $denied || ($access->superuser($actor) && $request->is('settings/profile') && $request->isMethod('PATCH')
|
||||
&& $request->has('email') && $request->input('email') !== $actor->email);
|
||||
if ($request->isMethod('DELETE') && $request->is('settings/profile')) {
|
||||
$denied = $denied || $access->superuser($actor)
|
||||
|| DB::table('portal_role_assignments')->where('user_id', $actor->id)->orWhere('approved_by', $actor->id)->exists()
|
||||
|| DB::table('portal_profile_requests')->where('user_id', $actor->id)->orWhere('decided_by', $actor->id)->exists();
|
||||
}
|
||||
if ($denied) {
|
||||
$access->audit($actor, 'privileged_identity_change_denied', ['method' => $request->method(), 'path' => $request->path()]);
|
||||
abort(403, 'Esta alteração removeria MFA obrigatório, um titular ou histórico de autorização. Use revogação ou substituição controlada; não desative MFA para recuperar acesso.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ProtectProfileOpportunity
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$id = null;
|
||||
if (preg_match('#^(?:(?:management|api/v1)/opportunities|api/v1/investment/admin/opportunities|minhas-oportunidades|management/territories/opportunities)/(\d+)(?:/|$)#', $request->path(), $match)) {
|
||||
$id = (int) $match[1];
|
||||
} elseif ($request->is('management/opportunity-origin/preview', 'management/opportunity-origin/apply')) {
|
||||
$id = filter_var($request->input('id'), FILTER_VALIDATE_INT) ?: null;
|
||||
} elseif ($request->is('api/v2/investment/admin/zones/*/links')) {
|
||||
$id = filter_var($request->input('opportunity_id'), FILTER_VALIDATE_INT) ?: null;
|
||||
}
|
||||
if ($request->user() && $id !== null && Schema::hasColumn('opportunities', 'owner_user_id')) {
|
||||
$record = Opportunity::withTrashed()->find($id);
|
||||
if ($record && $record->owner_user_id === null && ! $request->isMethod('GET')) {
|
||||
abort_unless(app(PortalProfileAccess::class)->legacyWriteAllowed($request->user()), 403, 'O papel v1 não contorna os controlos através de um registo legado.');
|
||||
}
|
||||
if ($record && ($record->owner_user_id !== null || str_starts_with($request->path(), 'minhas-oportunidades'))) {
|
||||
abort_if($record->owner_user_id === null && str_starts_with($request->path(), 'minhas-oportunidades'), 403, 'Registo legado não convertido para a política v1.');
|
||||
$access = app(PortalProfileAccess::class);
|
||||
$access->authorize($request->user(), $request->isMethod('GET') || str_ends_with($request->path(), '/workflow') || str_ends_with($request->path(), '/preview') ? 'view' : 'edit', $record);
|
||||
abort_if($request->is('management/opportunity-origin/apply'), 403, 'O cadastro privado v1 não usa a classificação editorial dos exemplos legados.');
|
||||
abort_if($request->isMethod('POST') && str_ends_with($request->path(), '/lifecycle') && ! str_contains($request->path(), '/resources/'), 403, 'Use o workflow com decisões segregadas; o perfil não concede exclusão.');
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user