From 003835463ba9ea5de4a8dd9c9e0a0ca31cc22f0a Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 24 Jun 2026 10:15:58 +0200 Subject: [PATCH] feat(audit) : capture IP/appareil/user-agent dans AuditLogger --- src/Service/AuditLogger.php | 22 ++++++++ tests/Service/AuditLoggerTest.php | 89 +++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 tests/Service/AuditLoggerTest.php diff --git a/src/Service/AuditLogger.php b/src/Service/AuditLogger.php index f0358f3..a5c638b 100644 --- a/src/Service/AuditLogger.php +++ b/src/Service/AuditLogger.php @@ -10,12 +10,15 @@ use App\Entity\User; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\RequestStack; readonly class AuditLogger { public function __construct( private EntityManagerInterface $entityManager, private Security $security, + private RequestStack $requestStack, + private UserAgentParser $userAgentParser, ) {} public function log( @@ -30,6 +33,21 @@ readonly class AuditLogger $user = $this->security->getUser(); $username = $user instanceof User ? $user->getUsername() : 'system'; + $request = $this->requestStack->getCurrentRequest(); + $ipAddress = null; + $userAgent = null; + $deviceId = null; + + if (null !== $request) { + $ipAddress = $request->getClientIp(); + $userAgent = $request->headers->get('User-Agent'); + $deviceId = $request->headers->get('X-Device-Id'); + // The device id comes from an untrusted client header; cap it to the column width. + if (null !== $deviceId) { + $deviceId = mb_substr($deviceId, 0, 64); + } + } + $auditLog = new AuditLog(); $auditLog ->setEmployee($employee) @@ -40,6 +58,10 @@ readonly class AuditLogger ->setDescription($description) ->setChanges($changes) ->setAffectedDate($affectedDate) + ->setIpAddress($ipAddress) + ->setUserAgent($userAgent) + ->setDeviceLabel($this->userAgentParser->parse($userAgent)) + ->setDeviceId($deviceId) ; $this->entityManager->persist($auditLog); diff --git a/tests/Service/AuditLoggerTest.php b/tests/Service/AuditLoggerTest.php new file mode 100644 index 0000000..97037a0 --- /dev/null +++ b/tests/Service/AuditLoggerTest.php @@ -0,0 +1,89 @@ +createMock(EntityManagerInterface::class); + $em->method('persist')->willReturnCallback(static function (object $entity) use (&$persisted): void { + $persisted = $entity; + }); + + $security = $this->createMock(Security::class); + $security->method('getUser')->willReturn(null); // -> username "system" + + $request = Request::create('/api/work_hours', 'POST'); + $request->server->set('REMOTE_ADDR', '203.0.113.7'); + $request->headers->set('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + $request->headers->set('X-Device-Id', 'device-abc'); + + $stack = new RequestStack(); + $stack->push($request); + + $logger = new AuditLogger($em, $security, $stack, new UserAgentParser()); + $logger->log(null, 'create', 'work_hour', 1, 'desc'); + + self::assertInstanceOf(AuditLog::class, $persisted); + self::assertSame('203.0.113.7', $persisted->getIpAddress()); + self::assertSame('device-abc', $persisted->getDeviceId()); + self::assertSame('Ordinateur · Windows · Chrome', $persisted->getDeviceLabel()); + self::assertNotNull($persisted->getUserAgent()); + } + + public function testTruncatesOverlongDeviceId(): void + { + $persisted = null; + $em = $this->createMock(EntityManagerInterface::class); + $em->method('persist')->willReturnCallback(static function (object $entity) use (&$persisted): void { + $persisted = $entity; + }); + $security = $this->createMock(Security::class); + $security->method('getUser')->willReturn(null); + + $request = Request::create('/api/work_hours', 'POST'); + $request->headers->set('X-Device-Id', str_repeat('x', 200)); + $stack = new RequestStack(); + $stack->push($request); + + $logger = new AuditLogger($em, $security, $stack, new UserAgentParser()); + $logger->log(null, 'create', 'work_hour', 1, 'desc'); + + self::assertSame(64, mb_strlen((string) $persisted->getDeviceId())); + } + + public function testNoRequestLeavesContextNull(): void + { + $persisted = null; + $em = $this->createMock(EntityManagerInterface::class); + $em->method('persist')->willReturnCallback(static function (object $entity) use (&$persisted): void { + $persisted = $entity; + }); + $security = $this->createMock(Security::class); + $security->method('getUser')->willReturn(null); + + $logger = new AuditLogger($em, $security, new RequestStack(), new UserAgentParser()); + $logger->log(null, 'create', 'work_hour', 1, 'desc'); + + self::assertNull($persisted->getIpAddress()); + self::assertNull($persisted->getUserAgent()); + self::assertNull($persisted->getDeviceLabel()); + self::assertNull($persisted->getDeviceId()); + } +}