*/ public static function fingerprintFiles(): array { return [__FILE__, app_path('Services/OpportunityClassification.php'), app_path('Http/Controllers/OpportunityClassificationController.php'), resource_path('js/components/opportunity-classification.tsx'), app_path('Services/OpportunityContacts.php'), resource_path('js/components/opportunity-contact-form.tsx'), app_path('Services/PortalInternalDesignation.php'), app_path('Models/User.php'), app_path('Models/Opportunity.php'), config_path('portal.php'), base_path('routes/web.php'), app_path('Http/Middleware/HandleInertiaRequests.php'), app_path('Services/OpportunityManagement.php'), app_path('Services/OpportunityResources.php'), app_path('Services/OpportunityImageProcessor.php'), app_path('Services/VerifiedDocumentBinary.php'), app_path('Services/PortalProfileAdministration.php'), app_path('Services/PortalSuperusers.php'), app_path('Services/OpportunityProfileWorkflow.php'), app_path('Http/Controllers/PortalProfileController.php'), app_path('Http/Controllers/DashboardController.php'), app_path('Http/Controllers/OpportunityManagementController.php'), app_path('Services/OpportunityCanonicalContract.php'), app_path('Services/OpportunityCanonicalResponse.php'), app_path('Services/OpportunityCategoryFields.php'), app_path('Services/OpportunityModularProfiles.php'), app_path('Services/OpportunityWorkflowTasks.php'), base_path('composer.json'), base_path('composer.lock'), app_path('Services/OpportunityReviewMateriality.php'), app_path('Services/OpportunityReviewConditions.php'), app_path('Services/OpportunityCanonicalMetadata.php'), app_path('Services/OpportunityCompleteness.php'), app_path('Services/OpportunityCanonicalValue.php'), resource_path('data/investmoz-opportunity-modular.schema.json'), resource_path('js/components/opportunity-canonical-fields.tsx'), resource_path('js/components/opportunity-parcel-geometry.tsx'), app_path('Http/Controllers/OpportunityResourceController.php'), app_path('Http/Controllers/InvestmentTerritoryController.php'), app_path('Http/Middleware/PortalProfileSession.php'), app_path('Http/Middleware/ProtectProfileOpportunity.php'), app_path('Http/Middleware/ProtectPrivilegedIdentity.php'), app_path('Http/Middleware/EnsurePortalProfiles.php'), app_path('Http/Middleware/AuditProfileDecision.php'), app_path('Http/Middleware/EnsurePortalProfileAdministration.php'), app_path('Providers/AppServiceProvider.php'), database_path('migrations/2026_10_10_040000_create_portal_profile_authorization.php'), base_path('bootstrap/app.php'), resource_path('js/pages/profiles/request.tsx'), resource_path('js/pages/profiles/administration.tsx'), resource_path('js/pages/profiles/audit.tsx'), resource_path('js/components/profile-navigation.tsx'), resource_path('js/components/opportunity-profile-actions.tsx'), resource_path('js/components/portal-grouped-menu.tsx'), resource_path('js/components/user-menu-content.tsx'), resource_path('js/pages/management/opportunities.tsx'), resource_path('js/pages/management/opportunity-draft.tsx'), resource_path('js/types/profile-access.ts'), resource_path('js/lib/profile-json.ts'), resource_path('js/pages/management/shared.tsx'), resource_path('js/pages/dashboard.tsx'), resource_path('js/pages/PortalInvestidor.tsx'), resource_path('js/layouts/portal-navigation.tsx'), resource_path('js/components/interactive-investment-map.tsx'), resource_path('js/components/simulation-form.tsx'), resource_path('js/lib/simulation-form.ts'), app_path('Services/LegacyContent.php'), resource_path('js/components/ui/sidebar.tsx'), resource_path('js/components/simulation-financial-tools.tsx'), resource_path('js/components/simulation-cashflow-planner.tsx'), resource_path('js/pages/investor-path/show.tsx'), resource_path('views/app.blade.php'), resource_path('views/profiles/creation-unavailable.blade.php'), resource_path('js/pages/management/opportunity-resources.tsx'), app_path('Services/PortalProfileInstallation.php'), app_path('Http/Controllers/PortalProfileInstallationController.php'), config_path('portal_profiles.php'), resource_path('js/pages/profiles/installation.tsx')]; } public function available(): bool { return config('portal.profile_access_enabled') && $this->installed(); } public function governanceAvailable(): bool { return (config('portal.profile_administration_enabled') || config('portal.profile_access_enabled')) && $this->installed(); } public function installed(): bool { return Schema::hasTable('portal_role_assignments') && Schema::hasTable('portal_superuser_pair') && Schema::hasTable('portal_profile_requests') && Schema::hasTable('portal_security_approvals') && Schema::hasColumn('users', 'portal_access_revision') && Schema::hasColumn('opportunities', 'workflow_context') && Schema::hasColumn('opportunities', 'owner_user_id') && Schema::hasColumn('opportunities', 'organization_id'); } /** @return list<\stdClass> */ public function grants(User $actor, ?string $role = null): array { if (! $this->governanceAvailable() || ! $actor->hasVerifiedEmail()) { return []; } return array_values(DB::table('portal_role_assignments')->where('user_id', $actor->id)->whereNull('revoked_at')->where('starts_at', '<=', now()) ->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))->when($role, fn ($q) => $q->where('role', $role))->get()->all()); } public function has(User $actor, string $role): bool { return $role === 'superuser' ? $this->superuser($actor) : $this->grants($actor, $role) !== []; } public function superuser(User $actor): bool { return $this->governanceAvailable() && $actor->hasVerifiedEmail() && DB::table('portal_superuser_pair')->where('id', 1) ->where(fn ($q) => $q->where('first_user_id', $actor->id)->orWhere('second_user_id', $actor->id))->exists(); } public function represents(User $actor, ?string $organization): bool { return $organization === null || DB::table('memberships as m')->join('organizations as o', 'o.id', '=', 'm.organization_id') ->where('m.user_id', $actor->id)->where('m.organization_id', $organization) ->whereNull('m.deleted_at')->whereNull('o.deleted_at')->whereIn('m.role', ['admin', 'manager', 'officer'])->exists(); } public function mfa(User $actor): bool { // Enrollment alone is insufficient: Fortify must have completed MFA in this session. return $actor->two_factor_confirmed_at !== null && $actor->two_factor_secret !== null && session('login.id') === null && session('portal.mfa_actor') === $actor->id && (int) session('portal.mfa_at', 0) >= time() - 900; } public function internalAssignment(User $actor, string $type): ?\stdClass { foreach ($this->grants($actor, 'promoter') as $grant) { if ($grant->source === 'internal-portal-designation' && in_array($type, json_decode($grant->opportunity_types, true), true) && app(PortalInternalDesignation::class)->organization($grant->organization_id) !== null) { return $grant; } } return null; } public function promoterRecordScope(User $actor, Opportunity $record): bool { foreach ($this->grants($actor, 'promoter') as $grant) { if (! in_array($record->opportunity_category, json_decode($grant->opportunity_types, true), true)) { continue; } if ($grant->source === 'internal-portal-designation') { if (app(PortalInternalDesignation::class)->organization($grant->organization_id) !== null && ($record->workflow_context['operator_organization_id'] ?? null) === $grant->organization_id && ((int) $record->owner_user_id === $actor->id || in_array($actor->id, $record->workflow_context['contributors'] ?? [], true))) { return true; } } elseif ($grant->organization_id === $record->organization_id && $this->represents($actor, $record->organization_id) && ($record->organization_id !== null || (int) $record->owner_user_id === $actor->id)) { return true; } } return false; } public function scoped(User $actor, string $role, ?string $organization, string $type): bool { if ($role === 'promoter' && $organization !== null && $this->internalAssignment($actor, $type) !== null && DB::table('organizations')->where('id', $organization)->whereNull('deleted_at')->exists()) { return true; } foreach ($this->grants($actor, $role) as $grant) { if (($grant->organization_id === $organization || ($role !== 'promoter' && $grant->organization_id === null)) && in_array($type, json_decode($grant->opportunity_types, true, 512, JSON_THROW_ON_ERROR), true)) { return $role !== 'promoter' || $this->represents($actor, $organization); } } return false; } public function operational(User $actor, string $action, ?int $recordId = null): bool { if (! $this->available()) { return false; } if ($this->has($actor, 'auditor')) { return false; } if (! $this->superuser($actor)) { return true; } // Normal Promotor authority comes from its existing scope, not from the // Superutilizador membership. Other/extraneous actions retain elevation. if (in_array($action, ['view', 'edit'], true) && $recordId !== null) { $record = Opportunity::withTrashed()->find($recordId); if ($record && $record->owner_user_id !== null && $this->promoterRecordScope($actor, $record)) { return true; } } if (! $this->mfa($actor)) { return false; } return DB::table('portal_security_approvals')->where('kind', 'elevation')->where('subject_user_id', $actor->id) ->whereNotNull('approved_by')->whereColumn('approved_by', '!=', 'subject_user_id') ->whereColumn('approved_by', '!=', 'requested_by')->whereNull('consumed_at')->where('expires_at', '>', now()) ->whereExists(fn ($q) => $q->selectRaw('1')->from('portal_superuser_pair') ->where(fn ($holders) => $holders->whereColumn('first_user_id', 'portal_security_approvals.approved_by')->orWhereColumn('second_user_id', 'portal_security_approvals.approved_by'))) ->where('scope->action', $action)->where('scope->record_id', $recordId)->exists(); } public function canCreate(User $actor, ?string $organization, string $type): bool { // Draft preparation is allowed within the assigned modality. Executable // submission support is checked separately by the canonical contract. $included = in_array($type, OpportunityManagement::CATEGORIES, true); // This is creation as an authorized Promotor, never an exceptional SU act. return $included && $this->available() && $this->scoped($actor, 'promoter', $organization, $type) && ! $this->has($actor, 'auditor'); } public function legacyWriteAllowed(User $actor): bool { // A deliberate v1 assignment cannot use the old allowlist to bypass its scope. if (! $actor->can('manage-portal')) { return false; } if (! Schema::hasTable('portal_role_assignments')) { return true; } // Revocation/expiry must not silently restore an older, wider privilege. return ! DB::table('portal_role_assignments')->where('user_id', $actor->id)->exists() && ! DB::table('portal_superuser_pair')->where(fn ($q) => $q->where('first_user_id', $actor->id)->orWhere('second_user_id', $actor->id))->exists() && ! DB::table('portal_security_approvals')->where('kind', 'replacement')->whereNotNull('consumed_at')->where('scope->old_user_id', $actor->id)->exists(); } /** Administration never grants operational access to a dossier or its attachments. */ public function hasOperationalReadScope(User $actor, Opportunity $record): bool { $personal = $record->organization_id !== null || (int) $record->owner_user_id === $actor->id; return $this->promoterRecordScope($actor, $record) || $this->scoped($actor, 'validator', $record->organization_id, $record->opportunity_category) || $this->scoped($actor, 'publisher', $record->organization_id, $record->opportunity_category); } public function canView(User $actor, Opportunity $record): bool { if ($record->owner_user_id !== null && ! $this->available()) { return false; } if ($this->superuser($actor)) { return ! $this->has($actor, 'auditor') && $this->hasOperationalReadScope($actor, $record) && $this->operational($actor, 'view', $record->id); } if ($record->owner_user_id === null) { return $actor->can('manage-portal'); } if (! $this->available() || ! $actor->hasVerifiedEmail()) { return false; } return $this->has($actor, 'auditor') || $this->hasOperationalReadScope($actor, $record); } public function canEdit(User $actor, Opportunity $record, ?string $newType = null): bool { if ($record->owner_user_id === null) { return $this->legacyWriteAllowed($actor); } return $this->canView($actor, $record) && $this->promoterRecordScope($actor, $record) && ! $record->trashed() && in_array($this->state($record), ['draft', 'needs_information'], true) && $this->scoped($actor, 'promoter', $record->organization_id, $newType ?? $record->opportunity_category) && $this->operational($actor, 'edit', $record->id); } public function conflict(User $actor, Opportunity $record): bool { $context = $record->workflow_context ?? []; return (int) $record->owner_user_id === $actor->id || in_array($actor->id, $context['contributors'] ?? [], true) || ($record->organization_id !== null && DB::table('memberships')->where('user_id', $actor->id) ->where('organization_id', $record->organization_id)->whereNull('deleted_at')->exists()); } public function state(Opportunity $record): string { return $record->workflow_context['working_state'] ?? $record->status; } public function authorize(User $actor, string $action, Opportunity $record): void { $allowed = $action === 'view' ? $this->canView($actor, $record) : $this->canEdit($actor, $record); if (! $allowed) { $this->audit($actor, 'access_denied', ['record_id' => $record->id, 'action' => $action]); } abort_unless($allowed, 403, 'O perfil, representação, tipo ou estado não autoriza esta operação.'); } /** * @param Builder $query * @return Builder */ public function scope(Builder $query, User $actor): Builder { if ($this->has($actor, 'auditor') && ! $this->superuser($actor)) { return $query; } if ($this->superuser($actor)) { $ids = DB::table('portal_security_approvals')->where('kind', 'elevation')->where('subject_user_id', $actor->id) ->where('scope->action', 'view')->whereNotNull('approved_by')->whereNull('consumed_at')->where('expires_at', '>', now()) ->get()->filter(fn ($row) => $this->operational($actor, 'view', json_decode($row->scope, true)['record_id'] ?? null)) ->map(fn ($row) => json_decode($row->scope, true)['record_id'])->all(); $query->where(function ($eligible) use ($ids, $actor) { $eligible->whereIn('id', $ids); if ($this->has($actor, 'auditor')) { return; } foreach ($this->grants($actor, 'promoter') as $grant) { $internal = $grant->source === 'internal-portal-designation'; if (($internal && app(PortalInternalDesignation::class)->organization($grant->organization_id) === null) || (! $internal && ! $this->represents($actor, $grant->organization_id))) { continue; } $eligible->orWhere(function ($normal) use ($actor, $grant, $internal) { $normal->whereNotNull('owner_user_id')->whereIn('opportunity_category', json_decode($grant->opportunity_types, true)); if ($internal) { $normal->where(fn ($q) => $q->where('owner_user_id', $actor->id)->orWhereJsonContains('workflow_context->contributors', $actor->id)) ->where('workflow_context->operator_organization_id', $grant->organization_id); } elseif ($grant->organization_id !== null) { $normal->where('organization_id', $grant->organization_id); } else { $normal->whereNull('organization_id')->where('owner_user_id', $actor->id); } }); } }); } return $query->where(function ($scope) use ($actor) { $scope->whereRaw('1=0'); if ($actor->can('manage-portal') && ! $this->superuser($actor)) { $scope->orWhereNull('owner_user_id'); } foreach ($this->grants($actor) as $grant) { if (! in_array($grant->role, ['promoter', 'validator', 'publisher'], true)) { continue; } if ($grant->role === 'promoter' && $grant->source !== 'internal-portal-designation' && ! $this->represents($actor, $grant->organization_id)) { continue; } if ($grant->role === 'promoter' && $grant->source === 'internal-portal-designation' && app(PortalInternalDesignation::class)->organization($grant->organization_id) === null) { continue; } $scope->orWhere(function ($allowed) use ($actor, $grant) { $allowed->whereNotNull('owner_user_id')->whereIn('opportunity_category', json_decode($grant->opportunity_types, true, 512, JSON_THROW_ON_ERROR)); if ($grant->role === 'promoter' && $grant->source === 'internal-portal-designation') { $allowed->where(fn ($q) => $q->where('owner_user_id', $actor->id)->orWhereJsonContains('workflow_context->contributors', $actor->id)) ->where('workflow_context->operator_organization_id', $grant->organization_id); } elseif ($grant->organization_id !== null) { $allowed->where('organization_id', $grant->organization_id); } elseif ($grant->role === 'promoter') { $allowed->whereNull('organization_id')->where('owner_user_id', $actor->id); } }); } }); } /** @return array|null */ public function navigation(?User $actor): ?array { if (! $actor || ! $this->governanceAvailable() || ! $actor->hasVerifiedEmail()) { return null; } $contexts = []; $seen = []; $internalPromoter = false; foreach ($this->grants($actor, 'promoter') as $grant) { if ($grant->source === 'internal-portal-designation' && app(PortalInternalDesignation::class)->organization($grant->organization_id) !== null) { $internalPromoter = true; foreach (DB::table('organizations')->whereNull('deleted_at')->get(['id', 'name']) as $realPromoter) { foreach (json_decode($grant->opportunity_types, true) as $type) { $key = $realPromoter->id.'|'.$type; if (! isset($seen[$key]) && $this->canCreate($actor, $realPromoter->id, $type)) { $contexts[] = ['organization_id' => $realPromoter->id, 'organization_name' => $realPromoter->name, 'type' => $type]; $seen[$key] = true; } } } continue; } if (! $this->represents($actor, $grant->organization_id)) { continue; } foreach (json_decode($grant->opportunity_types, true, 512, JSON_THROW_ON_ERROR) as $type) { $key = ($grant->organization_id ?? 'individual').'|'.$type; if (! isset($seen[$key]) && $this->canCreate($actor, $grant->organization_id, $type)) { $contexts[] = ['organization_id' => $grant->organization_id, 'organization_name' => $grant->organization_id ? DB::table('organizations')->where('id', $grant->organization_id)->value('name') : null, 'type' => $type]; $seen[$key] = true; } } } return ['contexts' => $contexts, 'internalPromoter' => $internalPromoter, 'mine' => $contexts ? '/minhas-oportunidades' : null, 'create' => $contexts ? '/minhas-oportunidades/create' : null, 'request' => '/perfil/promotor', 'review' => $this->available() && ($this->has($actor, 'validator') || $this->has($actor, 'publisher') || $this->has($actor, 'auditor') || $this->superuser($actor)) ? '/minhas-oportunidades' : null, 'audit' => $this->has($actor, 'auditor') || $this->superuser($actor) ? '/perfis/auditoria' : null, 'administration' => $this->superuser($actor) ? '/perfis/gestao' : null]; } /** @param array $data */ public function audit(User $actor, string $action, array $data): void { DB::table('audit_events')->insert(['source_system' => 'portal-profiles-v1', 'entity_type' => 'authorization', 'actor_user_id' => $actor->id, 'action' => $action, 'data_classification' => 'restricted', 'correlation_id' => (string) Str::uuid(), 'after_data' => json_encode([...$data, 'policy_version' => self::VERSION, 'origin_ip' => request()->ip()], JSON_THROW_ON_ERROR)]); } public function revokeSessions(User $subject): void { DB::table('users')->where('id', $subject->id)->increment('portal_access_revision'); // Invalidate remember-me as well; no password or MFA changes. DB::table('users')->where('id', $subject->id)->update(['remember_token' => null]); if (Schema::hasTable('sessions')) { DB::table('sessions')->where('user_id', $subject->id)->delete(); } if (Schema::hasTable('personal_access_tokens')) { DB::table('personal_access_tokens')->where('tokenable_type', User::class)->where('tokenable_id', $subject->id)->delete(); } } }