src/Controller/IntegrationController.php line 537

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Usuario;
  4. use App\Entity\ViewProfile;
  5. use App\Entity\Periodo;
  6. use Doctrine\ORM\EntityManagerInterface;
  7. use Doctrine\Persistence\ManagerRegistry;
  8. use Psr\Log\LoggerInterface;
  9. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  10. use Symfony\Component\HttpFoundation\JsonResponse;
  11. use Symfony\Component\HttpFoundation\Request;
  12. use Symfony\Component\HttpFoundation\Response;
  13. use Symfony\Component\Routing\Annotation\Route;
  14. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  15. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  16. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  17. use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
  18. use Firebase\JWT\JWT;
  19. use Firebase\JWT\Key;
  20. use Firebase\JWT\ExpiredException;
  21. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  22. #[Route('/api/integration')]
  23. class IntegrationController extends AbstractController
  24. {
  25.     private EntityManagerInterface $em;
  26.     private ManagerRegistry $doctrine;
  27.     private string $secretKey;
  28.     private TokenStorageInterface $tokenStorage;
  29.     private SessionInterface $session;
  30.     private string $systemName;
  31.     private LoggerInterface $logger;
  32.     // Constante para mapeo de iconos
  33.     private const ICON_MAP = [
  34.         'fa-chart-line' => 'DotChartOutlined',
  35.         'fa-users-cog' => 'TeamOutlined',
  36.         'fa-book' => 'BookOutlined',
  37.         'fa-calendar-alt' => 'CalendarOutlined',
  38.         'fa-users' => 'UsergroupAddOutlined',
  39.         'fa-home' => 'HomeOutlined',
  40.         'fa-cogs' => 'SettingOutlined',
  41.         'fa-angle-double-up' => 'ArrowUpOutlined',
  42.         'fa-user' => 'UserOutlined',
  43.         'fa-file' => 'FileOutlined',
  44.         'fa-search' => 'SearchOutlined',
  45.         'fa-bell' => 'BellOutlined',
  46.         'fa-dashboard' => 'DashboardOutlined',
  47.         'fa-appstore' => 'AppstoreOutlined',
  48.         'fa-area-chart' => 'AreaChartOutlined',
  49.         'fa-bar-chart' => 'BarChartOutlined'
  50.     ];
  51.     public function __construct(
  52.         EntityManagerInterface $em,
  53.         ManagerRegistry $doctrine,
  54.         TokenStorageInterface $tokenStorage,
  55.         SessionInterface $session,
  56.         LoggerInterface $logger
  57.     ) {
  58.         $this->em $em;
  59.         $this->doctrine $doctrine;
  60.         $this->tokenStorage $tokenStorage;
  61.         $this->session $session;
  62.         $this->logger $logger;
  63.         $this->secretKey $_ENV['SERVICIOS_ADMINISTRATIVOS_SECRET_KEY'] ?? '';
  64.         $this->systemName $_ENV['SYSTEM_NAME'] ?? 'UNKNOWN_SYSTEM';
  65.         if (empty($this->secretKey)) {
  66.             throw new \RuntimeException('SERVICIOS_ADMINISTRATIVOS_SECRET_KEY no configurada');
  67.         }
  68.     }
  69.     /**
  70.      * Endpoint principal para SSO Login
  71.      * Valida token JWT y establece sesión siguiendo la misma lógica del login normal
  72.      */
  73.     #[Route('/sso-login'name'integration_sso_login'methods: ['GET'])]
  74.     public function ssoLogin(Request $request): Response
  75.     {
  76.         $token $request->query->get('token');
  77.         $redirectPath $request->query->get('redirect');
  78.         if (!$token) {
  79.             $this->logger->warning('SSO Login: Token no proporcionado');
  80.             return $this->jsonError('Token SSO es requerido'''400);
  81.         }
  82.         try {
  83.             // 1. Decodificar y validar token JWT
  84.             $decoded $this->decodeAndValidateJWT($token);
  85.             if (!isset($decoded->email)) {
  86.                 throw new \Exception('Token inválido: falta email');
  87.             }
  88.             // 2. Buscar usuario
  89.             $user $this->findUserByEmail($decoded->email);
  90.             if (!$user) {
  91.                 $this->logger->info('SSO Login: Usuario no encontrado', [
  92.                     'email' => $decoded->email
  93.                 ]);
  94.                 return $this->render('restringido.html.twig', [], new Response(''403));
  95.             }
  96.             // 3. VALIDACIONES DEL USUARIO (misma lógica que login normal)
  97.             $this->validateUserForLogin($user);
  98.             // 4. VALIDAR SI YA EXISTE SESIÓN ACTIVA VÁLIDA
  99.             if ($this->hasValidSession($user)) {
  100.                 $this->logger->info('SSO Login: Sesión existente válida, redirigiendo', [
  101.                     'user_id' => $user->getIdUsu(),
  102.                     'email' => $user->getCorreo()
  103.                 ]);
  104.                 
  105.                 $targetPath $this->determineRedirectPath($redirectPath);
  106.                 return $this->redirect($targetPath '?embedded=1&sso=1');
  107.             }
  108.             // 5. No hay sesión válida, crear una nueva
  109.             $this->logger->info('SSO Login: Creando nueva sesión', [
  110.                 'user_id' => $user->getIdUsu(),
  111.                 'email' => $user->getCorreo()
  112.             ]);
  113.             // ***  Invalidar cualquier sesión previa ***
  114.             $this->invalidatePreviousSession();
  115.             $this->authenticateUserInSession($user$request);
  116.             
  117.             $targetPath $this->determineRedirectPath($redirectPath);
  118.             return $this->redirect($targetPath '?embedded=1&sso=1');
  119.         } catch (ExpiredException $e) {
  120.             $this->logger->warning('SSO Login: Token expirado', [
  121.                 'error' => $e->getMessage()
  122.             ]);
  123.             return $this->jsonError(
  124.                 'Token SSO expirado',
  125.                 'Por favor, vuelve a intentar desde el sistema principal',
  126.                 401
  127.             );
  128.         } catch (CustomUserMessageAuthenticationException $e) {
  129.             // Excepción de validación de usuario
  130.             $this->logger->warning('SSO Login: Validación de usuario falló', [
  131.                 'error' => $e->getMessage()
  132.             ]);
  133.             return $this->render('restringido.html.twig', [
  134.                 'mensaje' => $e->getMessage()
  135.             ], new Response(''403));
  136.         } catch (\Exception $e) {
  137.             $this->logger->error('SSO Login: Error en autenticación', [
  138.                 'error' => $e->getMessage(),
  139.                 'trace' => $e->getTraceAsString()
  140.             ]);
  141.             
  142.             return $this->jsonError('Error en autenticación SSO'$e->getMessage(), 401);
  143.         }
  144.     }
  145.     /**
  146.      * Verifica si el usuario tiene una sesión activa y válida
  147.      */
  148.     private function hasValidSession(Usuario $user): bool
  149.     {
  150.         $currentToken $this->tokenStorage->getToken();
  151.         
  152.         if (!$currentToken) {
  153.             return false;
  154.         }
  155.         $sessionUser $currentToken->getUser();
  156.         
  157.         // Verificar que sea un Usuario válido y que coincida con el email
  158.         if (!$sessionUser instanceof Usuario) {
  159.             return false;
  160.         }
  161.         if ($sessionUser->getCorreo() !== $user->getCorreo()) {
  162.             return false;
  163.         }
  164.         // Verificar que los datos de sesión críticos existan
  165.         if (!$this->session->has('perfil') || !$this->session->has('menu')) {
  166.             $this->logger->warning('Sesión sin datos críticos, regenerando', [
  167.                 'user_id' => $user->getIdUsu()
  168.             ]);
  169.             return false;
  170.         }
  171.         return true;
  172.     }
  173.     /**
  174.      * Decodifica y valida el token JWT
  175.      */
  176.     private function decodeAndValidateJWT(string $token): object
  177.     {
  178.         try {
  179.             $decoded JWT::decode($token, new Key($this->secretKey'HS256'));
  180.             if (!isset($decoded->email) || empty($decoded->email)) {
  181.                 throw new \Exception('Token inválido: falta email');
  182.             }
  183.             if (isset($decoded->exp) && $decoded->exp time()) {
  184.                 throw new ExpiredException('Token expirado');
  185.             }
  186.             if (isset($decoded->iss)) {
  187.                 $expectedIssuer $_ENV['JWT_ISSUER'] ?? 'integration-service';
  188.                 if ($decoded->iss !== $expectedIssuer) {
  189.                     $this->logger->warning('Emisor del token inválido', [
  190.                         'expected' => $expectedIssuer,
  191.                         'received' => $decoded->iss
  192.                     ]);
  193.                     throw new \Exception('Emisor del token inválido');
  194.                 }
  195.             }
  196.             if (isset($decoded->system)) {
  197.                 if ($decoded->system !== $this->systemName) {
  198.                     $this->logger->warning('Token no destinado a este sistema', [
  199.                         'expected_system' => $this->systemName,
  200.                         'token_system' => $decoded->system
  201.                     ]);
  202.                     throw new \Exception('Token no destinado a este sistema');
  203.                 }
  204.             }
  205.             $this->logger->debug('Token JWT validado exitosamente', [
  206.                 'email' => $decoded->email,
  207.                 'system' => $decoded->system ?? 'N/A'
  208.             ]);
  209.             return $decoded;
  210.         } catch (ExpiredException $e) {
  211.             throw $e;
  212.         } catch (\Exception $e) {
  213.             $this->logger->error('Error decodificando JWT', [
  214.                 'error' => $e->getMessage()
  215.             ]);
  216.             throw new \Exception('Token inválido: ' $e->getMessage());
  217.         }
  218.     }
  219.     /**
  220.      * Busca un usuario por email
  221.      */
  222.     private function findUserByEmail(string $email): ?Usuario
  223.     {
  224.         return $this->em->getRepository(Usuario::class)
  225.             ->findOneBy(['correo' => $email]);
  226.     }
  227.     /**
  228.      * Busca un usuario por email o ID
  229.      */
  230.     private function findUserByEmailOrId(?string $email, ?int $userId): ?Usuario
  231.     {
  232.         if ($userId) {
  233.             return $this->em->getRepository(Usuario::class)->find($userId);
  234.         }
  235.         
  236.         if ($email) {
  237.             return $this->findUserByEmail($email);
  238.         }
  239.         return null;
  240.     }
  241.     /**
  242.      * Valida que el usuario pueda iniciar sesión
  243.      * *** CORREGIDO: Misma lógica exacta que LoginAuthenticator ***
  244.      * 
  245.      * @throws CustomUserMessageAuthenticationException si el usuario no puede iniciar sesión
  246.      */
  247.     private function validateUserForLogin(Usuario $user): void
  248.     {
  249.         // Validación 1: Usuario Pendiente
  250.         if ($user->getEstatus() === 'Pendiente') {
  251.             $this->logger->warning('Intento de login SSO con usuario pendiente', [
  252.                 'user_id' => $user->getIdUsu(),
  253.                 'email' => $user->getCorreo()
  254.             ]);
  255.             throw new CustomUserMessageAuthenticationException('Access request pending evaluation.');
  256.         }
  257.         // Validación 2: Usuario Rechazado
  258.         if ($user->getEstatus() === 'Rechazado') {
  259.             $this->logger->warning('Intento de login SSO con usuario rechazado', [
  260.                 'user_id' => $user->getIdUsu(),
  261.                 'email' => $user->getCorreo()
  262.             ]);
  263.             throw new CustomUserMessageAuthenticationException('Access request has been rejected.');
  264.         }
  265.         // Validación 3: Usuario Suspendido
  266.         if ($user->getEstatus() === 'Suspendido') {
  267.             $this->logger->warning('Intento de login SSO con usuario suspendido', [
  268.                 'user_id' => $user->getIdUsu(),
  269.                 'email' => $user->getCorreo()
  270.             ]);
  271.             throw new CustomUserMessageAuthenticationException('Access credentials suspended.');
  272.         }
  273.         $this->logger->debug('Usuario validado correctamente para SSO login', [
  274.             'user_id' => $user->getIdUsu(),
  275.             'email' => $user->getCorreo(),
  276.             'estatus' => $user->getEstatus()
  277.         ]);
  278.     }
  279.     /**
  280.      * Establece la autenticación del usuario en la sesión
  281.      */
  282.     private function authenticateUserInSession(Usuario $userRequest $request): void
  283.     {
  284.         try {
  285.             // *** AGREGADO: Actualizar estatus de "Aceptado" a "Activo" en primer login ***
  286.             $estatusActual $user->getEstatus();
  287.             if ($estatusActual === "Aceptado") {
  288.                 $user->setEstatus("Activo");
  289.                 $this->em->persist($user);
  290.                 $this->em->flush();
  291.                 
  292.                 $this->logger->info('Estatus actualizado de Aceptado a Activo', [
  293.                     'user_id' => $user->getIdUsu()
  294.                 ]);
  295.             }
  296.             // 1. Crear y establecer token de autenticación
  297.             $token = new UsernamePasswordToken($user'main'$user->getRoles());
  298.             $this->tokenStorage->setToken($token);
  299.             // 2. Obtener periodo actual
  300.             $periodo $this->em->getRepository(Periodo::class)->findOneBy(['actual' => 1]);
  301.             $this->session->set('periodo'$periodo);
  302.         
  303.             // 3. Construir perfil y menú (MISMA LÓGICA QUE LoginAuthenticator)
  304.             $rol $user->getRol()->getIdRol();
  305.             $acciones $this->em->getRepository(ViewProfile::class)->findBy(['idRol' => $rol]);
  306.             
  307.             $perfil = [];
  308.             $menu = [];
  309.             
  310.             foreach ($acciones as $accion) {
  311.                 // *** CORREGIDO: Construcción de perfil con índice por idMod ***
  312.                 $perfil[$accion->getIdMod()] = [
  313.                     "idMod" => $accion->getIdMod(),
  314.                     "nombre" => $accion->getModulo(),
  315.                     "submenu" => $accion->getSubmenu(),
  316.                     "idBeh" => $accion->getIdBeh(),
  317.                     "nivel" => $accion->getNivel(),
  318.                     "comportamiento" => $accion->getComportamiento(),
  319.                     "descripcion" => $accion->getDescripcion()
  320.                 ];
  321.                 
  322.                 // *** Construcción del menú lateral ***
  323.                 if ($accion->getNivel() > 0) {
  324.                     if (empty($accion->getSubmenu())) {
  325.                         $menu[$accion->getOrden()] = [
  326.                             "idMenu" => $accion->getIdMenu(),
  327.                             "menu" => $accion->getModulo(),
  328.                             "icono" => $accion->getIcono(),
  329.                             "ruta" => $accion->getRuta(),
  330.                             "submenu" => false
  331.                         ];
  332.                     } else {
  333.                         if (!array_key_exists($accion->getOrden(), $menu)) {
  334.                             $menu[$accion->getOrden()] = [
  335.                                 "idMenu" => $accion->getIdMenu(),
  336.                                 "menu" => $accion->getModulo(),
  337.                                 "icono" => $accion->getIcono(),
  338.                                 "ruta" => "#"
  339.                             ];
  340.                         }
  341.                         $menu[$accion->getOrden()]["submenu"][] = [
  342.                             "idMenu" => $accion->getIdSubmenu(),
  343.                             "menu" => $accion->getSubmenu(),
  344.                             "icono" => "fa-angle-double-up",
  345.                             "ruta" => $accion->getRuta()
  346.                         ];
  347.                     }
  348.                 }
  349.             }
  350.             
  351.             ksort($menu);
  352.             
  353.             // 4. Establecer datos en sesión
  354.             $this->session->set('perfil'$perfil);
  355.             $this->session->set('menu'$menu);
  356.             $this->logger->info('Sesión establecida exitosamente con estructura original', [
  357.                 'user_id' => $user->getIdUsu(),
  358.                 'email' => $user->getCorreo(),
  359.                 'perfil_items' => count($perfil),
  360.                 'menu_items' => count($menu)
  361.             ]);
  362.         }
  363.         catch (\Exception $e) {
  364.             $this->logger->error('Error validando token de integración', [
  365.                 'error' => $e->getMessage()
  366.             ]); 
  367.         } 
  368.     }
  369.     /**
  370.      * Determina la ruta de redirección validando seguridad
  371.      */
  372.     private function determineRedirectPath(?string $redirectPath): string
  373.     {
  374.         if (empty($redirectPath)) {
  375.             return '/';
  376.         }
  377.         $redirectPath trim($redirectPath);
  378.         // Prevenir redirecciones externas
  379.         if (preg_match('#^(https?:)?//#i'$redirectPath)) {
  380.             $this->logger->warning('Intento de redirección externa bloqueado', [
  381.                 'redirect' => $redirectPath
  382.             ]);
  383.             return '/';
  384.         }
  385.         $redirectPath ltrim($redirectPath'/');
  386.         
  387.         try {
  388.             return $this->generateUrl($redirectPath);
  389.         } catch (RouteNotFoundException $e) {
  390.             $this->logger->warning('Ruta Symfony no encontrada', [
  391.                 'redirect' => $redirectPath
  392.             ]);
  393.             
  394.             return '/';
  395.         }
  396.     }
  397.     /**
  398.      * Valida el token de integración en requests API
  399.      */
  400.     private function validateIntegrationToken(Request $request): ?object
  401.     {
  402.         $authHeader $request->headers->get('Authorization');
  403.         
  404.         if (!$authHeader || !preg_match('/Bearer\s+(.*)$/i'$authHeader$matches)) {
  405.             $this->logger->warning('Token de integración ausente o malformado');
  406.             return null;
  407.         }
  408.         try {
  409.             return $this->decodeAndValidateJWT($matches[1]);
  410.         } catch (\Exception $e) {
  411.             $this->logger->error('Error validando token de integración', [
  412.                 'error' => $e->getMessage()
  413.             ]);
  414.             return null;
  415.         }
  416.     }
  417.     /**
  418.      * Invalida completamente la sesión anterior
  419.     */
  420.     private function invalidatePreviousSession(): void
  421.     {
  422.         $currentToken $this->tokenStorage->getToken();
  423.         
  424.         if ($currentToken && $currentToken->getUser() instanceof Usuario) {
  425.             $this->tokenStorage->setToken(null);
  426.             $this->session->invalidate();
  427.             $this->logger->debug('Sesión de otro usuario invalidada completamente');
  428.         } else {
  429.             $this->session->migrate(true);
  430.             $this->logger->debug('ID de sesión regenerado (sin sesión previa)');
  431.         }
  432.     }
  433.     // ==================== ENDPOINTS API ====================
  434.     #[Route('/test'name'integration_test'methods: ['GET'])]
  435.     public function test(): JsonResponse
  436.     {
  437.         return new JsonResponse([
  438.             'status' => 'OK',
  439.             'message' => 'Integration endpoint working correctly',
  440.             'system_name' => $this->systemName,
  441.             'timestamp' => date('Y-m-d H:i:s')
  442.         ]);
  443.     }
  444.     #[Route('/check-user'name'integration_check_user'methods: ['POST'])]
  445.     public function checkUser(Request $request): JsonResponse
  446.     {
  447.         if (!$this->validateIntegrationToken($request)) {
  448.             return $this->jsonError('Invalid or missing integration token'''401);
  449.         }
  450.         $data json_decode($request->getContent(), true);
  451.         $email $data['email'] ?? null;
  452.         if (!$email) {
  453.             return $this->jsonError('Email is required'''400);
  454.         }
  455.         try {
  456.             $user $this->findUserByEmail($email);
  457.             if (!$user) {
  458.                 return new JsonResponse([
  459.                     'has_access' => false,
  460.                     'system_name' => $this->systemName,
  461.                     'message' => 'User not found in this system'
  462.                 ]);
  463.             }
  464.             return new JsonResponse([
  465.                 'has_access' => true,
  466.                 'user_id' => $user->getIdUsu(),
  467.                 'email' => $user->getCorreo(),
  468.                 'name' => trim($user->getNombre() . ' ' $user->getPapellido()),
  469.                 'roles' => [$user->getRol()->getNombre()],
  470.                 'system_name' => $this->systemName
  471.             ]);
  472.         } catch (\Exception $e) {
  473.             $this->logger->error('Error en check-user', [
  474.                 'email' => $email,
  475.                 'error' => $e->getMessage()
  476.             ]);
  477.             return $this->jsonError('Database error'$e->getMessage(), 500);
  478.         }
  479.     }
  480.     #[Route('/get-menu'name'integration_get_menu'methods: ['POST'])]
  481.     public function getMenu(Request $request): JsonResponse
  482.     {
  483.         if (!$this->validateIntegrationToken($request)) {
  484.             return $this->jsonError('Invalid or missing integration token'''401);
  485.         }
  486.         $data json_decode($request->getContent(), true);
  487.         $email $data['email'] ?? null;
  488.         $userId $data['user_id'] ?? null;
  489.         if (!$email && !$userId) {
  490.             return $this->jsonError('Email or user_id is required'''400);
  491.         }
  492.         try {
  493.             $user $this->findUserByEmailOrId($email$userId);
  494.             if (!$user) {
  495.                 return new JsonResponse([
  496.                     'menu' => [],
  497.                     'message' => 'User not found'
  498.                 ]);
  499.             }
  500.             $menu $this->buildUserMenuFromSession($user);
  501.             $frontendMenu $this->transformMenuToFrontend($menu);
  502.             return new JsonResponse([
  503.                 'user_name' => trim($user->getNombre() . ' ' $user->getPapellido()),
  504.                 'menu_tree' => $frontendMenu,
  505.                 'user_id' => $user->getIdUsu(),
  506.                 'system_name' => $this->systemName
  507.             ]);
  508.         } catch (\Exception $e) {
  509.             $this->logger->error('Error en get-menu', [
  510.                 'email' => $email,
  511.                 'user_id' => $userId,
  512.                 'error' => $e->getMessage()
  513.             ]);
  514.             return $this->jsonError('Error building menu'$e->getMessage(), 500);
  515.         }
  516.     }
  517.     /**
  518.      * Construye el menú desde la sesión (para API endpoints)
  519.      */
  520.     private function buildUserMenuFromSession(Usuario $user): array
  521.     {
  522.         $rol $user->getRol()->getIdRol();
  523.         $acciones $this->em->getRepository(ViewProfile::class)->findBy(['idRol' => $rol]);
  524.         
  525.         $menu = [];
  526.         
  527.         foreach ($acciones as $accion) {
  528.             if ($accion->getNivel() > 0) {
  529.                 if (empty($accion->getSubmenu())) {
  530.                     $menu[$accion->getOrden()] = [
  531.                         "idMenu" => $accion->getIdMenu(),
  532.                         "menu" => $accion->getModulo(),
  533.                         "icono" => $accion->getIcono(),
  534.                         "ruta" => $accion->getRuta(),
  535.                         "submenu" => false
  536.                     ];
  537.                 } else {
  538.                     if (!array_key_exists($accion->getOrden(), $menu)) {
  539.                         $menu[$accion->getOrden()] = [
  540.                             "idMenu" => $accion->getIdMenu(),
  541.                             "menu" => $accion->getModulo(),
  542.                             "icono" => $accion->getIcono(),
  543.                             "ruta" => "#"
  544.                         ];
  545.                     }
  546.                     $menu[$accion->getOrden()]["submenu"][] = [
  547.                         "idMenu" => $accion->getIdSubmenu(),
  548.                         "menu" => $accion->getSubmenu(),
  549.                         "icono" => "fa-angle-double-up",
  550.                         "ruta" => $accion->getRuta()
  551.                     ];
  552.                 }
  553.             }
  554.         }
  555.         
  556.         ksort($menu);
  557.         return array_values($menu);
  558.     }
  559.     /**
  560.      * Transforma el menú al formato del frontend
  561.      */
  562.     private function transformMenuToFrontend(array $menuint $startKey 2): array
  563.     {
  564.         $keyCounter $startKey;
  565.         
  566.         $transform = function ($item) use (&$transform, &$keyCounter) {
  567.             $children = [];
  568.             if (!empty($item['submenu']) && is_array($item['submenu'])) {
  569.                 foreach ($item['submenu'] as $subItem) {
  570.                     $children[] = $transform($subItem);
  571.                 }
  572.             }
  573.             $path = ($item['ruta'] === '#' || $item['ruta'] === null)
  574.                 ? null
  575.                 '/' ltrim($item['ruta'], '/');
  576.             return [
  577.                 'key' => $keyCounter++,
  578.                 'label' => $item['menu'] ?? '',
  579.                 'icon' => $this->mapIcon($item['icono'] ?? ''),
  580.                 'path' => $path,
  581.                 'external' => true,
  582.                 'children' => $children
  583.             ];
  584.         };
  585.         $result array_map($transform$menu);
  586.         return [[
  587.             'key' => 1,
  588.             'label' => 'Sigmec',
  589.             'icon' => 'UsergroupAddOutlined',
  590.             'path' => null,
  591.             'external' => true,
  592.             'children' => $result
  593.         ]];
  594.     }
  595.     private function mapIcon(string $faIcon): ?string
  596.     {
  597.         return self::ICON_MAP[$faIcon] ?? 'RightOutlined';
  598.     }
  599.     /**
  600.      * Método helper para respuestas de error consistentes
  601.      */
  602.     private function jsonError(string $errorstring $message ''int $status 400): JsonResponse
  603.     {
  604.         $response = [
  605.             'error' => $error,
  606.             'system_name' => $this->systemName
  607.         ];
  608.         if (!empty($message)) {
  609.             $response['message'] = $message;
  610.         }
  611.         return new JsonResponse($response$status);
  612.     }
  613. }