190 lines
11 KiB
PHP
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);
|
|
}
|
|
}
|