PasswordChangeSubscriber::notifyUser()   A
last analyzed

Complexity

Conditions 2
Paths 6

Size

Total Lines 41
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 28
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 41
rs 9.472
1
<?php
2
3
namespace ProjetNormandie\UserBundle\EventSubscriber;
4
5
use Doctrine\ORM\EntityManagerInterface;
6
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface;
7
use ProjetNormandie\UserBundle\Entity\RefreshToken;
8
use ProjetNormandie\UserBundle\Security\Event\SecurityEventTypeEnum;
9
use ProjetNormandie\UserBundle\Security\SecurityHistoryManager;
10
use Psr\Log\LoggerInterface;
11
use ProjetNormandie\UserBundle\Event\PasswordChangedEvent;
12
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
13
use Symfony\Component\Mailer\MailerInterface;
14
use Symfony\Component\Mime\Email;
15
use Symfony\Contracts\Translation\TranslatorInterface;
16
use Symfony\Component\HttpFoundation\RequestStack;
17
18
class PasswordChangeSubscriber implements EventSubscriberInterface
19
{
20
    public function __construct(
21
        private readonly LoggerInterface $logger,
22
        private readonly MailerInterface $mailer,
23
        private readonly EntityManagerInterface $entityManager,
24
        private readonly TranslatorInterface $translator,
25
        private readonly RequestStack $requestStack,
26
        private readonly SecurityHistoryManager $securityHistoryManager,
27
        private readonly RefreshTokenManagerInterface $refreshTokenManager,
28
    ) {
29
    }
30
31
    /**
32
     * @return array<string, array<int, array<int, int|string>>>
33
     */
34
    public static function getSubscribedEvents(): array
35
    {
36
        return [
37
            PasswordChangedEvent::class => [
38
                ['logPasswordChange', 100],
39
                ['notifyUser', 90],
40
                ['enforceSecurityMeasures', 80],
41
            ],
42
        ];
43
    }
44
45
    public function logPasswordChange(PasswordChangedEvent $event): void
46
    {
47
        $user = $event->getUser();
48
        $request = $this->requestStack->getCurrentRequest();
49
        $ip = $request ? $request->getClientIp() : 'unknown';
50
51
        $this->logger->info(
52
            sprintf(
53
                '##PASSWORD_CHANGED##[IP=%s/user=%s/id=%d]',
54
                $ip,
55
                $user->getUsername(),
56
                $user->getId()
57
            ),
58
            [
59
                'user_id' => $user->getId(),
60
                'username' => $user->getUsername(),
61
                'ip' => $ip,
62
                'action' => 'password_change',
63
                'timestamp' => new \DateTime()
64
            ]
65
        );
66
    }
67
68
    public function notifyUser(PasswordChangedEvent $event): void
69
    {
70
        $user = $event->getUser();
71
72
        $this->logger->debug('Sending password change notification', [
73
            'user_id' => $user->getId(),
74
            'username' => $user->getUsername()
75
        ]);
76
77
        try {
78
            $locale = $user->getLanguage();
79
80
            $emailBody = $this->translator->trans(
81
                'password_change.notification',
82
                [
83
                    '%username%' => $user->getUsername(),
84
                    '%date%' => (new \DateTime())->format('d/m/Y H:i:s')
85
                ],
86
                'email',
87
                $locale
88
            );
89
90
            $email = (new Email())
91
                ->to($user->getEmail())
92
                ->subject($this->translator->trans('password_change.subject', [], 'email', $locale))
93
                ->text($emailBody)
94
                ->html($emailBody);
95
96
            $this->mailer->send($email);
97
98
            $this->logger->info('Password change notification sent successfully', [
99
                'user_id' => $user->getId(),
100
                'username' => $user->getUsername(),
101
                'email' => $user->getEmail()
102
            ]);
103
        } catch (\Exception $e) {
104
            $this->logger->error('Error sending password change notification', [
105
                'user_id' => $user->getId(),
106
                'username' => $user->getUsername(),
107
                'error' => $e->getMessage(),
108
                'trace' => $e->getTraceAsString()
109
            ]);
110
        }
111
    }
112
113
114
    public function enforceSecurityMeasures(PasswordChangedEvent $event): void
115
    {
116
        $user = $event->getUser();
117
118
        $this->logger->debug('Applying security measures after password change', [
119
            'user_id' => $user->getId(),
120
            'username' => $user->getUsername()
121
        ]);
122
123
        // Revoke all refresh tokens for JWT authentication
124
        $username = $user->getUsername();
125
126
        $tokens = $this->entityManager->getRepository(RefreshToken::class)
127
            ->createQueryBuilder('t')
128
            ->where('t.username = :username')
129
            ->setParameter('username', $username)
130
            ->getQuery()
131
            ->getResult();
132
133
        foreach ($tokens as $token) {
134
            $this->refreshTokenManager->delete($token);
135
        }
136
137
        $this->logger->info('Revoked all JWT refresh tokens after password change', [
138
            'user_id' => $user->getId(),
139
            'token_count' => count($tokens)
140
        ]);
141
142
143
        // Record password change in security history
144
        $this->securityHistoryManager->recordEvent(
145
            $user,
146
            SecurityEventTypeEnum::PASSWORD_CHANGE,
147
            [
148
                'timestamp' => new \DateTime(),
149
            ]
150
        );
151
152
        $this->logger->info('Recorded password change in security history', [
153
            'user_id' => $user->getId()
154
        ]);
155
    }
156
}
157