DB::table('portal_profile_requests')->where('user_id', $request->user()->id)->latest()->get(), 'types' => OpportunityManagement::CATEGORIES, 'organizations' => DB::table('memberships as m')->join('organizations as o', 'o.id', '=', 'm.organization_id')->where('m.user_id', $request->user()->id) ->whereNull('m.deleted_at')->whereNull('o.deleted_at')->whereIn('m.role', ['admin', 'manager', 'officer'])->get(['o.id', 'o.name']), 'documents' => DB::table('documents')->where('uploaded_by', $request->user()->id)->whereNull('deleted_at')->where('status', 'active')->where('malware_scan_status', 'clean') ->whereColumn('sha256', 'malware_scanned_sha256')->get(['id', 'file_name']), 'grants' => $access->grants($request->user(), 'promoter')]); } public function requestProfile(Request $request, PortalProfileAdministration $service): JsonResponse { return response()->json(['id' => $service->request($request->user(), $request->all()), 'message' => 'Pedido recebido; perfil ainda não ativado.'], 201); } public function requestStatus(Request $request, string $id): JsonResponse { $row = DB::table('portal_profile_requests')->where('id', $id)->where('user_id', $request->user()->id)->first(); abort_unless($row !== null, 403); return response()->json($row); } public function administration(Request $request, PortalProfileAdministration $service): Response { $service->administrative($request->user()); return Inertia::render('profiles/administration', ['technicalSupportAvailable' => app(OpportunityTechnicalSupport::class)->installed(), 'requests' => DB::table('portal_profile_requests')->latest()->get(), 'assignments' => DB::table('portal_role_assignments')->get(), 'pair' => DB::table('portal_superuser_pair')->first(), 'operatorOrganizations' => app(PortalInternalDesignation::class)->organizations(), 'staffAccounts' => User::query()->select(['id', 'name', 'email', 'email_verified_at']) ->selectRaw('CASE WHEN two_factor_secret IS NOT NULL AND two_factor_confirmed_at IS NOT NULL THEN 1 ELSE 0 END AS mfa_enrolled') ->withCasts(['mfa_enrolled' => 'boolean'])->orderBy('name')->get()->map(fn (User $user) => [ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'verified' => $user->email_verified_at !== null, 'mfa' => $user->getAttribute('mfa_enrolled'), ]), 'internalTypes' => OpportunityCanonicalContract::BASE_CATEGORIES, 'actorId' => $request->user()->id, 'types' => OpportunityManagement::CATEGORIES, 'organizations' => DB::table('organizations')->whereNull('deleted_at')->get(['id', 'name']), 'securityRequests' => DB::table('portal_security_approvals')->latest()->limit(30)->get(), 'securityAlerts' => DB::table('audit_events')->where('source_system', 'portal-profiles-v1')->whereIn('action', ['superuser_pair_initialized', 'superuser_replaced', 'role_revoked'])->latest('occurred_at')->limit(20)->get(['id', 'action', 'actor_user_id', 'occurred_at'])]); } public function decide(Request $request, string $id, PortalProfileAdministration $service): JsonResponse { $service->decide($request->user(), $id, $request->all()); return response()->json(['message' => 'Decisão registada; somente os tipos aprovados ficam autorizados.']); } public function audit(Request $request, PortalProfileAccess $access): Response|JsonResponse { abort_unless($access->has($request->user(), 'auditor') || ($access->superuser($request->user()) && $access->mfa($request->user())), 403); $filters = $request->validate(['actor_id' => 'nullable|integer|min:1', 'action' => 'nullable|string|max:100', 'record_id' => 'nullable|integer|min:1', 'from' => 'nullable|date', 'until' => 'nullable|date|after_or_equal:from', 'format' => 'nullable|in:json']); $query = DB::table('audit_events')->when($filters['actor_id'] ?? null, fn ($q, $v) => $q->where('actor_user_id', $v)) ->when($filters['action'] ?? null, fn ($q, $v) => $q->where('action', $v)) ->when($filters['record_id'] ?? null, fn ($q, $v) => $q->where('after_data->record_id', $v)) ->when($filters['from'] ?? null, fn ($q, $v) => $q->where('occurred_at', '>=', $v)) ->when($filters['until'] ?? null, fn ($q, $v) => $q->where('occurred_at', '<=', $v)); // Minimized metadata: never decrypt private snapshots or export authentication bodies. $query->select(['id', 'source_system', 'action', 'actor_user_id', 'entity_type', 'occurred_at', 'correlation_id']); if (($filters['format'] ?? null) === 'json') { $records = $query->orderByDesc('occurred_at')->limit(1000)->get(); $access->audit($request->user(), 'audit_metadata_exported', ['filters' => $filters, 'count' => $records->count()]); return response()->json(['filters' => $filters, 'records' => $records, 'limit' => 1000])->header('Content-Disposition', 'attachment; filename="audit-metadata.json"')->header('Cache-Control', 'private, no-store'); } $records = $query->orderByDesc('occurred_at')->paginate(30)->withQueryString(); $access->audit($request->user(), 'audit_metadata_read', ['filters' => $filters, 'count' => $records->count()]); return Inertia::render('profiles/audit', ['records' => $records, 'filters' => $filters]); } public function registerOperatorOrganization(Request $request, PortalInternalDesignation $service): JsonResponse { return response()->json($service->registerOrganization($request->user(), $request->all()), 201); } public function grant(Request $request, PortalProfileAdministration $service): JsonResponse { return response()->json(['id' => $service->grant($request->user(), $request->all())], 201); } public function requestDocument(Request $request, string $id, string $documentId, PortalProfileAdministration $service): \Illuminate\Http\Response|BinaryFileResponse { $service->administrative($request->user()); $row = DB::table('portal_profile_requests')->where('id', $id)->first(); abort_unless($row && in_array($documentId, json_decode($row->document_ids, true), true), 404); $document = DB::table('documents')->where('id', $documentId)->first(); abort_unless($document !== null, 404); return app(VerifiedDocumentBinary::class)->respond($document, true); } public function revoke(Request $request, string $id, PortalProfileAdministration $service): JsonResponse { $service->revoke($request->user(), $id, (string) $request->input('reason')); return response()->json(['message' => 'Papel revogado; sessões e tokens anteriores invalidados.']); } public function opportunities(Request $request, PortalProfileAccess $access): Response { abort_unless($access->grants($request->user()) !== [] || $access->superuser($request->user()), 403); return app(OpportunityManagementController::class)->index($request, app(OpportunityManagement::class)); } public function edit(Request $request, PortalProfileAccess $access, ?int $id = null): Response|\Symfony\Component\HttpFoundation\Response { if ($id) { $access->authorize($request->user(), str_ends_with($request->path(), '/view') ? 'view' : 'edit', Opportunity::withTrashed()->findOrFail($id)); } else { if (! ($access->navigation($request->user())['contexts'] ?? [])) { $reason = $access->has($request->user(), 'auditor') ? 'O papel de Auditor permite consulta e impede a criação de oportunidades.' : 'Esta conta não dispõe de um contexto ativo de Promotor para uma organização e modalidade autorizadas. Confirme a vigência, o âmbito e os tipos atribuídos na gestão de perfis. A elevação excecional de Superutilizador não substitui a autorização de Promotor.'; if ($request->expectsJson()) { abort(403, $reason); } return response()->view('profiles.creation-unavailable', ['reason' => $reason], 403); } } return app(OpportunityManagementController::class)->edit($request, app(OpportunityManagement::class), $id); } public function workflow(Request $request, int $id, OpportunityWorkflowExecution $service): JsonResponse { try { $record = $service->transition($request->user(), $id, $request->all()); } catch (OpportunityWorkflowUnavailable $failure) { return response()->json(['message' => 'Não foi possível confirmar o resultado. Consulte o estado da oportunidade antes de repetir. '.($failure->recorded ? 'Incidente técnico registado. ' : 'Não foi possível guardar o incidente; conserve esta referência. ').'Referência: '.$failure->reference, 'reference' => $failure->reference, 'incident_recorded' => $failure->recorded], 503)->header('Cache-Control', 'private, no-store'); } return response()->json(['id' => $id, 'status' => app(PortalProfileAccess::class)->state($record), 'publication_status' => $record->status, 'etag' => app(OpportunityManagement::class)->etag($record), 'message' => 'Ação interna registada; não representa decisão externa.']); } public function contact(Request $request, int $id, OpportunityContacts $service): JsonResponse { return response()->json($service->save($request->user(), $id, $request->all())); } public function replacement(Request $request, PortalSuperusers $service): JsonResponse { $data = $request->validate(['old_user_id' => 'required|integer', 'new_user_id' => 'required|integer', 'reason' => 'required|string|min:20|max:1000']); return response()->json(['id' => $service->requestReplacement($request->user(), $data['old_user_id'], $data['new_user_id'], $data['reason'])], 201); } public function approveSecurity(Request $request, string $id, PortalSuperusers $service): JsonResponse { $service->approve($request->user(), $id); return response()->json(['message' => 'Aprovação independente registada.']); } public function replace(Request $request, string $id, PortalSuperusers $service): JsonResponse { $service->applyReplacement($request->user(), $id); return response()->json(['message' => 'Titular substituído atomicamente; continuam dois titulares.']); } public function elevation(Request $request, PortalSuperusers $service): JsonResponse { $data = $request->validate(['action' => 'required|string', 'record_id' => 'nullable|integer|exists:opportunities,id', 'reason' => 'required|string|min:20|max:1000']); return response()->json(['id' => $service->requestElevation($request->user(), $data['action'], $data['record_id'] ?? null, $data['reason'])], 201); } }