Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
PasswordResetController
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
7 / 7
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 postReset
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
2
 resetPassword
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 postResetPassword
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 showView
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requireToken
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Engelsystem\Controllers;
6
7use Engelsystem\Helpers\Carbon;
8use Engelsystem\Http\Exceptions\HttpNotFound;
9use Engelsystem\Http\Request;
10use Engelsystem\Http\Response;
11use Engelsystem\Mail\EngelsystemMailer;
12use Engelsystem\Models\User\PasswordReset;
13use Engelsystem\Models\User\User;
14use Psr\Log\LoggerInterface;
15use Symfony\Component\HttpFoundation\Session\SessionInterface;
16
17class PasswordResetController extends BaseController
18{
19    use HasUserNotifications;
20
21    /** @var array<string, string> */
22    protected array $permissions = [
23        'reset'             => 'login',
24        'postReset'         => 'login',
25        'resetPassword'     => 'login',
26        'postResetPassword' => 'login',
27    ];
28
29    public function __construct(
30        protected Response $response,
31        protected SessionInterface $session,
32        protected EngelsystemMailer $mail,
33        protected LoggerInterface $log
34    ) {
35    }
36
37    public function reset(): Response
38    {
39        return $this->showView('pages/password/reset');
40    }
41
42    public function postReset(Request $request): Response
43    {
44        $data = $this->validate($request, [
45            'email' => 'required|email',
46        ]);
47
48        /** @var User $user */
49        $user = User::whereEmail($data['email'])->first();
50        if ($user) {
51            $reset = (new PasswordReset())->findOrNew($user->id);
52            $reset->user_id = $user->id;
53            $reset->token = bin2hex(random_bytes(16));
54            $reset->created_at = Carbon::now();
55            $reset->save();
56
57            $this->log->info(
58                sprintf('Password recovery for %s (%u)', $user->name, $user->id),
59                ['user' => $user->toJson()]
60            );
61
62            $this->mail->sendViewTranslated(
63                $user,
64                'Password recovery',
65                'emails/password-reset',
66                ['username' => $user->displayName, 'reset' => $reset]
67            );
68        }
69
70        return $this->showView('pages/password/reset-success', ['type' => 'email']);
71    }
72
73    public function resetPassword(Request $request): Response
74    {
75        $this->requireToken($request);
76
77        return $this->showView(
78            'pages/password/reset-form',
79            ['min_length' => config('password_min_length')]
80        );
81    }
82
83    public function postResetPassword(Request $request): Response
84    {
85        $reset = $this->requireToken($request);
86
87        $data = $this->validate($request, [
88            'password'              => 'required|length:' . config('password_min_length'),
89            'password_confirmation' => 'required',
90        ]);
91
92        if ($data['password'] !== $data['password_confirmation']) {
93            $this->addNotification('validation.password.confirmed', NotificationType::ERROR);
94
95            return $this->showView('pages/password/reset-form');
96        }
97
98        auth()->setPassword($reset->user, $data['password']);
99        $reset->delete();
100
101        $reset->user->sessions()->getQuery()->delete();
102
103        return $this->showView('pages/password/reset-success', ['type' => 'reset']);
104    }
105
106    protected function showView(string $view = 'pages/password/reset', array $data = []): Response
107    {
108        return $this->response->withView($view, $data);
109    }
110
111    protected function requireToken(Request $request): PasswordReset
112    {
113        $token = $request->getAttribute('token');
114
115        /** @var PasswordReset|null $reset */
116        $reset = PasswordReset::whereToken($token)
117            ->where('created_at', '>=', Carbon::now()->subHours(.5))
118            ->first();
119
120        if (!$reset) {
121            throw new HttpNotFound();
122        }
123
124        return $reset;
125    }
126}