Files
Investidor-V2/app/Http/Controllers/PortalProfileController.php
T

190 lines
11 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\OpportunityCanonicalContract;
use App\Services\OpportunityContacts;
use App\Services\OpportunityManagement;
use App\Services\OpportunityProfileWorkflow;
use App\Services\PortalInternalDesignation;
use App\Services\PortalProfileAccess;
use App\Services\PortalProfileAdministration;
use App\Services\PortalSuperusers;
use App\Services\VerifiedDocumentBinary;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class PortalProfileController extends Controller
{
public function requests(Request $request, PortalProfileAccess $access): Response
{
return Inertia::render('profiles/request', ['requests' => 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', ['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(),
'internalTypes' => OpportunityCanonicalContract::BASE_CATEGORIES,
'staffAccounts' => User::orderBy('name')->get(['id', 'name', 'email', 'email_verified_at', 'two_factor_confirmed_at'])
->map(fn ($user) => ['id' => $user->id, 'name' => $user->name, 'email' => $user->email,
'verified' => $user->hasVerifiedEmail(), 'mfa' => $user->two_factor_confirmed_at !== null]),
'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, OpportunityProfileWorkflow $service): JsonResponse
{
$record = $service->transition($request->user(), $id, $request->all());
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);
}
}