diff --git a/CHANGELOG.md b/CHANGELOG.md index 0db93f5..2d3566e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,31 @@ # Changelog +## [0.8.2] - 2025-03-04 +### 🔹 Added +- Nueva funcionalidad para tener notificaciones en tiempo real. Instalación de bundle "Mercure". +- Nuevo endpoint "backup image". Integracion con ogRepository. + +### ⚡ Changed +- Cambios en logs. Cambios en salida (stderror -> file.log) + +--- +## [0.8.1] - 2025-02-25 +### 🐛 Fixed +- Corrección de bug en el deploy de imágenes + +--- + +## [0.8.0] - 2025-01-10 +### 🔹 Added +- Nuevos campos en "aulas" para la jerarquia en clientes. +- Nueva funcionalidad "imagen global". Integracion con ogRepository. + +### ⚡ Changed +- Limpieza en campos "name" y "date" de ogLive. Es necesario parsear el campo "filename" para facilitar el uso al usuario en la web. +### 🐛 Fixed +- Corrección de bug que impedia borrar un cliente si tenia una traza enlazada. +- +--- + ## [0.7.3] - 2025-01-03 ### 🔹 Added @@ -7,13 +34,9 @@ - Se agregó la funcionalidad de borrar imágenes. Integración con ogRepository. - Se agregó el modo "TORRENT" y "UDPCAST" en el despliegue de imágenes. -### 🛠️ Fixed - ### ⚡ Changed - Refactorización del webhook de ogRepository. -### 🛑 Removed - --- ## Formato de cambios: diff --git a/config/services/api_platform.yaml b/config/services/api_platform.yaml index 2240693..379733d 100644 --- a/config/services/api_platform.yaml +++ b/config/services/api_platform.yaml @@ -20,7 +20,7 @@ services: api_platform.filter.client.search: parent: 'api_platform.doctrine.orm.search_filter' - arguments: [ { 'id': 'exact', 'name': 'partial', 'serialNumber': 'exact', 'template.id': 'exact', organizationalUnit.id: 'exact', mac: 'exact', ip: 'exact' } ] + arguments: [ { 'id': 'exact', 'name': 'partial', 'serialNumber': 'exact', 'template.id': 'exact', status: 'exact', organizationalUnit.id: 'exact', mac: 'exact', ip: 'exact', subnet.id: 'exact' } ] tags: [ 'api_platform.filter' ] api_platform.filter.client.exist: diff --git a/migrations/Version20250225081416.php b/migrations/Version20250225081416.php new file mode 100644 index 0000000..2c72bcd --- /dev/null +++ b/migrations/Version20250225081416.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE network_settings ADD netiface VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE network_settings DROP netiface'); + } +} diff --git a/migrations/Version20250227095120.php b/migrations/Version20250227095120.php new file mode 100644 index 0000000..8aef7e3 --- /dev/null +++ b/migrations/Version20250227095120.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE user ADD groups_view VARCHAR(255) NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE user DROP groups_view'); + } +} diff --git a/src/Controller/OgBoot/PxeBootFile/PostAction.php b/src/Controller/OgBoot/PxeBootFile/PostAction.php index 73ce878..b2245b8 100644 --- a/src/Controller/OgBoot/PxeBootFile/PostAction.php +++ b/src/Controller/OgBoot/PxeBootFile/PostAction.php @@ -46,7 +46,7 @@ class PostAction extends AbstractOgBootController 'router' => $client->getOrganizationalUnit()->getNetworkSettings()->getRouter(), 'netmask' => $client->getOrganizationalUnit()->getNetworkSettings() ? $client->getOrganizationalUnit()->getNetworkSettings()->getNetmask() : '255.255.255.0', 'computer_name' => $client->getName(), - 'netiface' => $client->getNetiface(), + 'netiface' => $client->getNetiface() ? $client->getNetiface() : $client->getOrganizationalUnit()->getNetworkSettings()->getNetiface(), 'group' => $client->getOrganizationalUnit()->getName(), 'ogrepo' => $ogRepoIp, 'ogcore' => $this->ogCoreIP, diff --git a/src/Controller/OgDhcp/Subnet/PostHostAction.php b/src/Controller/OgDhcp/Subnet/PostHostAction.php index e2f4027..ba9de31 100644 --- a/src/Controller/OgDhcp/Subnet/PostHostAction.php +++ b/src/Controller/OgDhcp/Subnet/PostHostAction.php @@ -29,27 +29,54 @@ class PostHostAction extends AbstractOgDhcpController */ public function __invoke(SubnetAddHostInput $input, Subnet $subnet): JsonResponse { - $client = $input->client; + $clients = $input->clients; - /** @var Client $clientEntity */ - $clientEntity = $client->getEntity(); + $success = []; + $errors = []; - $data = [ - 'host' => $clientEntity->getName(), - 'macAddress' => strtolower($clientEntity->getMac()), - 'address' => $clientEntity->getIp(), - ]; + foreach ($clients as $client) { + /** @var Client $clientEntity */ + $clientEntity = $client->getEntity(); - $params = [ - 'json' => $data - ]; + $data = [ + 'host' => $clientEntity->getName(), + 'macAddress' => strtolower($clientEntity->getMac()), + 'address' => $clientEntity->getIp(), + ]; - $content = $this->createRequest('POST', 'http://'.$this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$subnet->getServerId().'/hosts', $params); + $params = ['json' => $data]; - $subnet->addClient($clientEntity); - $this->entityManager->persist($subnet); - $this->entityManager->flush(); + try { + $content = $this->createRequest( + 'POST', + 'http://' . $this->ogDhcpApiUrl . '/ogdhcp/v1/subnets/' . $subnet->getServerId() . '/hosts', + $params + ); - return new JsonResponse(data: $content, status: Response::HTTP_OK); + // Guardar resultado exitoso + $success[] = [ + 'client' => $clientEntity->getName(), + 'response' => $content + ]; + + // Persistir solo si la llamada fue exitosa + $subnet->addClient($clientEntity); + $this->entityManager->persist($subnet); + $this->entityManager->flush(); + } catch (\Throwable $e) { // Capturar cualquier error sin interrumpir + $errors[] = [ + 'client' => $clientEntity->getName(), + 'error' => $e->getMessage() + ]; + } + } + + return new JsonResponse( + [ + 'success' => $success, + 'errors' => $errors + ], + empty($errors) ? Response::HTTP_OK : Response::HTTP_MULTI_STATUS + ); } } \ No newline at end of file diff --git a/src/Dto/Input/NetworkSettingsInput.php b/src/Dto/Input/NetworkSettingsInput.php index 46cec1f..fe3ef70 100644 --- a/src/Dto/Input/NetworkSettingsInput.php +++ b/src/Dto/Input/NetworkSettingsInput.php @@ -41,6 +41,9 @@ class NetworkSettingsInput #[Groups(['organizational-unit:write'])] public ?string $ntp = null; + #[Groups(['organizational-unit:write'])] + public ?string $netiface = null; + #[OrganizationalUnitP2PMode] #[Groups(['organizational-unit:write'])] public ?string $p2pMode = null; @@ -93,6 +96,7 @@ class NetworkSettingsInput $this->netmask = $networkSettings->getNetmask(); $this->router = $networkSettings->getRouter(); $this->ntp = $networkSettings->getNtp(); + $this->netiface = $networkSettings->getNetiface(); $this->p2pMode = $networkSettings->getP2pMode(); $this->p2pTime = $networkSettings->getP2pTime(); $this->mcastIp = $networkSettings->getMcastIp(); @@ -130,6 +134,7 @@ class NetworkSettingsInput $networkSettings->setDns($this->dns); $networkSettings->setNetmask($this->netmask); $networkSettings->setRouter($this->router); + $networkSettings->setNetiface($this->netiface); $networkSettings->setNtp($this->ntp); $networkSettings->setP2pMode($this->p2pMode); $networkSettings->setP2pTime($this->p2pTime); diff --git a/src/Dto/Input/SubnetAddHostInput.php b/src/Dto/Input/SubnetAddHostInput.php index 0129a86..d903357 100644 --- a/src/Dto/Input/SubnetAddHostInput.php +++ b/src/Dto/Input/SubnetAddHostInput.php @@ -12,7 +12,10 @@ use Symfony\Component\Validator\Constraints as Assert; final class SubnetAddHostInput { + /** + * @var ClientOutput[] + */ #[Assert\NotNull] #[Groups(['subnet:write'])] - public ?ClientOutput $client = null; + public ?array $clients = []; } \ No newline at end of file diff --git a/src/Dto/Input/UserInput.php b/src/Dto/Input/UserInput.php index 5ecc8bb..aea6947 100644 --- a/src/Dto/Input/UserInput.php +++ b/src/Dto/Input/UserInput.php @@ -29,6 +29,9 @@ final class UserInput #[Groups('user:write')] public ?string $password = null; + #[Groups('user:write')] + public ?string $groupsView = null; + #[Assert\NotNull] #[Groups('user:write')] public ?bool $enabled = true; @@ -63,6 +66,7 @@ final class UserInput $this->username = $user->getUsername(); $this->enabled= $user->isEnabled(); + $this->groupsView = $user->getGroupsView(); if ($user->getUserGroups()) { foreach ($user->getUserGroups() as $userGroup) { @@ -85,6 +89,7 @@ final class UserInput $user->setUsername($this->username); $user->setEnabled($this->enabled); + $user->setGroupsView($this->groupsView); foreach ($this->userGroups as $userGroup) { $userGroupsToAdd[] = $userGroup->getEntity(); diff --git a/src/Dto/Output/NetworkSettingsOutput.php b/src/Dto/Output/NetworkSettingsOutput.php index 2224045..00c089a 100644 --- a/src/Dto/Output/NetworkSettingsOutput.php +++ b/src/Dto/Output/NetworkSettingsOutput.php @@ -30,6 +30,9 @@ final class NetworkSettingsOutput extends AbstractOutput #[Groups(['network-settings:read', "organizational-unit:read", "client:read"])] public ?string $ntp = null; + #[Groups(['network-settings:read', "organizational-unit:read", "client:read"])] + public ?string $netiface = null; + #[Groups(['network-settings:read', "organizational-unit:read", "client:read"])] public ?string $p2pMode = null; @@ -80,6 +83,7 @@ final class NetworkSettingsOutput extends AbstractOutput $this->netmask = $networkSettings->getNetmask(); $this->router = $networkSettings->getRouter(); $this->ntp = $networkSettings->getNtp(); + $this->netiface = $networkSettings->getNetiface(); $this->p2pMode = $networkSettings->getP2pMode(); $this->p2pTime = $networkSettings->getP2pTime(); $this->mcastIp = $networkSettings->getMcastIp(); diff --git a/src/Dto/Output/OrganizationalUnitOutput.php b/src/Dto/Output/OrganizationalUnitOutput.php index b77decf..7adb715 100644 --- a/src/Dto/Output/OrganizationalUnitOutput.php +++ b/src/Dto/Output/OrganizationalUnitOutput.php @@ -38,7 +38,7 @@ final class OrganizationalUnitOutput extends AbstractOutput #[Groups(['organizational-unit:read'])] public ?self $parent = null; - #[Groups(['organizational-unit:read', 'organizational-unit:read:collection:short'])] + #[Groups(['organizational-unit:read', 'client:read', 'organizational-unit:read:collection:short'])] public string $path; #[Groups(['organizational-unit:read', "client:read"])] diff --git a/src/Dto/Output/UserOutput.php b/src/Dto/Output/UserOutput.php index 7960699..cdab043 100644 --- a/src/Dto/Output/UserOutput.php +++ b/src/Dto/Output/UserOutput.php @@ -23,6 +23,8 @@ final class UserOutput extends AbstractOutput public array $allowedOrganizationalUnits; #[Groups(['user:read'])] public array $userGroups; + #[Groups(['user:read'])] + public string $groupsView; #[Groups(['user:read'])] public \DateTime $createdAt; @@ -37,6 +39,7 @@ final class UserOutput extends AbstractOutput $this->username = $user->getUsername(); $this->roles = $user->getRoles(); $this->enabled = $user->isEnabled(); + $this->groupsView = $user->getGroupsView(); $this->userGroups = $user->getUserGroups()->map( fn(UserGroup $userGroup) => new UserGroupOutput($userGroup) diff --git a/src/Entity/NetworkSettings.php b/src/Entity/NetworkSettings.php index a62e8ed..bab7b65 100644 --- a/src/Entity/NetworkSettings.php +++ b/src/Entity/NetworkSettings.php @@ -31,6 +31,9 @@ class NetworkSettings extends AbstractEntity #[ORM\Column(length: 255, nullable: true)] private ?string $ntp = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $netiface = null; + #[ORM\Column(nullable: true)] private ?int $p2pTime = null; @@ -161,6 +164,18 @@ class NetworkSettings extends AbstractEntity return $this; } + public function getNetiface(): ?string + { + return $this->netiface; + } + + public function setNetiface(?string $netiface): static + { + $this->netiface = $netiface; + + return $this; + } + public function getP2pTime(): ?int { return $this->p2pTime; diff --git a/src/Entity/User.php b/src/Entity/User.php index 4b821ce..984b0e9 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -51,6 +51,9 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate private ?string $newPassword = null; private ?string $repeatNewPassword = null; + #[ORM\Column(length: 255)] + private ?string $groupsView = null; + public function __construct() { @@ -245,4 +248,16 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate return $this; } + + public function getGroupsView(): ?string + { + return $this->groupsView; + } + + public function setGroupsView(string $groupsView): static + { + $this->groupsView = $groupsView; + + return $this; + } } diff --git a/src/Factory/UserFactory.php b/src/Factory/UserFactory.php index 01cca77..b29a018 100644 --- a/src/Factory/UserFactory.php +++ b/src/Factory/UserFactory.php @@ -32,6 +32,7 @@ final class UserFactory extends ModelFactory return [ 'password' => $hash, 'roles' => [], + 'groupsView' => 'card', 'username' => self::faker()->text(180), ]; } diff --git a/src/Formatter/CustomLineFormatter.php b/src/Formatter/CustomLineFormatter.php index ee8b6bc..f9ab5ca 100644 --- a/src/Formatter/CustomLineFormatter.php +++ b/src/Formatter/CustomLineFormatter.php @@ -19,6 +19,7 @@ class CustomLineFormatter extends LineFormatter 'component' => 'ogcore', 'params' => $record['context'], 'desc' => $record['message'], + 'datetime' => $record['datetime']->format('Y-m-d H:i:s'), ]; return json_encode($output, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL; diff --git a/src/Lexik/JWTCreatedListener.php b/src/Lexik/JWTCreatedListener.php index 34ca078..ae37549 100644 --- a/src/Lexik/JWTCreatedListener.php +++ b/src/Lexik/JWTCreatedListener.php @@ -23,6 +23,7 @@ class JWTCreatedListener $payload['username'] = $user->getUsername(); $payload['uuid'] = $user->getUuid(); $payload['roles'] = $user->getRoles(); + $payload['groupsView'] = $user->getGroupsView(); $event->setData($payload); } diff --git a/src/Repository/ClientRepository.php b/src/Repository/ClientRepository.php index c94c0c2..4e96160 100644 --- a/src/Repository/ClientRepository.php +++ b/src/Repository/ClientRepository.php @@ -16,23 +16,24 @@ class ClientRepository extends AbstractRepository parent::__construct($registry, Client::class); } - public function findClientsByOrganizationalUnitAndDescendants(int $organizationalUnitId): array + public function findClientsByOrganizationalUnitAndDescendants(int $organizationalUnitId, array $filters = []): array { - $query = $this->getEntityManager()->createQuery( + $entityManager = $this->getEntityManager(); + + $query = $entityManager->createQuery( 'SELECT o.path FROM App\Entity\OrganizationalUnit o WHERE o.id = :id' )->setParameter('id', $organizationalUnitId); $result = $query->getOneOrNullResult(); - if (!$result) { return []; } $path = $result['path'] . '/%'; - $query = $this->getEntityManager()->createQuery( + $query = $entityManager->createQuery( 'SELECT o.id FROM App\Entity\OrganizationalUnit o WHERE o.id = :id OR o.path LIKE :path' @@ -42,13 +43,32 @@ class ClientRepository extends AbstractRepository $unitIds = array_column($query->getArrayResult(), 'id'); - $query = $this->getEntityManager()->createQuery( - 'SELECT c - FROM App\Entity\Client c - WHERE c.organizationalUnit IN (:unitIds)' - ) + $qb = $entityManager->createQueryBuilder(); + $qb->select('c') + ->from(Client::class, 'c') + ->where('c.organizationalUnit IN (:unitIds)') ->setParameter('unitIds', $unitIds); - return $query->getResult(); + foreach ($filters as $key => $value) { + if ($key === 'order' || $key === 'page' || $key === 'itemsPerPage' || $key === 'organizationalUnit.id') { + continue; + } + + if ($key === 'exists') { + foreach ($value as $field => $existsValue) { + if ($existsValue === 'true') { + $qb->andWhere("c.$field IS NOT NULL"); + } elseif ($existsValue === 'false') { + $qb->andWhere("c.$field IS NULL"); + } + } + } else { + $qb->andWhere("c.$key = :$key")->setParameter($key, $value); + } + } + + return $qb->getQuery()->getResult(); } + + } diff --git a/src/State/Provider/ClientProvider.php b/src/State/Provider/ClientProvider.php index 364e8da..a6b0fad 100644 --- a/src/State/Provider/ClientProvider.php +++ b/src/State/Provider/ClientProvider.php @@ -39,10 +39,11 @@ readonly class ClientProvider implements ProviderInterface public function provideCollection(Operation $operation, array $uriVariables = [], array $context = []): object { - $organizationalUnitId = $context['filters']['organizationalUnit.id'] ?? null; + $filters = $context['filters'] ?? []; - if ($organizationalUnitId) { - $clients = $this->clientRepository->findClientsByOrganizationalUnitAndDescendants((int) $organizationalUnitId); + if (isset($filters['organizationalUnit.id'])) { + $organizationalUnitId = (int) $filters['organizationalUnit.id']; + $clients = $this->clientRepository->findClientsByOrganizationalUnitAndDescendants($organizationalUnitId, $filters); $items = new \ArrayObject(); foreach ($clients as $client) { @@ -50,12 +51,20 @@ readonly class ClientProvider implements ProviderInterface } return new TraversablePaginator($items, 1, count($clients), count($clients)); - } + } else { + $paginator = $this->collectionProvider->provide($operation, $uriVariables, $context); - return $this->collectionProvider->provide($operation, $uriVariables, $context); + $items = new \ArrayObject(); + foreach ($paginator->getIterator() as $item) { + $items[] = new ClientOutput($item); + } + + return new TraversablePaginator($items, $paginator->getCurrentPage(), $paginator->getItemsPerPage(), $paginator->getTotalItems()); + } } + public function provideItem(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { $item = $this->itemProvider->provide($operation, $uriVariables, $context);