• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / auth / 22527357078

28 Feb 2026 07:22PM UTC coverage: 63.267% (+0.7%) from 62.568%
22527357078

push

github

daycry
Remove PHP 8.1 from PHPUnit CI matrix

Update .github/workflows/phpunit.yml to drop PHP 8.1 from the test matrix. CI will now run PHPUnit only on PHP 8.2 and 8.3, reducing the matrix to current supported versions.

3064 of 4843 relevant lines covered (63.27%)

41.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Controllers/PasswordResetController.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Auth\Controllers;
15

16
use CodeIgniter\Events\Events;
17
use CodeIgniter\HTTP\IncomingRequest;
18
use CodeIgniter\HTTP\RedirectResponse;
19
use CodeIgniter\HTTP\ResponseInterface;
20
use CodeIgniter\I18n\Time;
21
use Daycry\Auth\Enums\IdentityType;
22
use Daycry\Auth\Models\LoginModel;
23
use Daycry\Auth\Models\UserIdentityModel;
24
use Daycry\Auth\Models\UserModel;
25

26
/**
27
 * Handles password reset flow via email token.
28
 *
29
 * The user requests a reset link, receives an email with a token,
30
 * and uses that token to set a new password.
31
 */
32
class PasswordResetController extends BaseAuthController
33
{
34
    /**
35
     * @var UserModel
36
     */
37
    protected $provider;
38

39
    public function __construct()
×
40
    {
41
        /** @var class-string<UserModel> $providerClass */
42
        $providerClass = setting('Auth.userProvider');
×
43

44
        $this->provider = new $providerClass();
×
45
    }
46

47
    /**
48
     * Displays the form to request a password reset email.
49
     */
50
    public function requestView(): ResponseInterface
×
51
    {
52
        if (($redirect = $this->redirectIfLoggedIn()) instanceof RedirectResponse) {
×
53
            return $redirect;
×
54
        }
55

56
        $content = $this->view(setting('Auth.views')['password-reset-request']);
×
57

58
        return $this->response->setBody($content);
×
59
    }
60

61
    /**
62
     * Receives the email, generates a reset token, and sends an email.
63
     */
64
    public function requestAction(): RedirectResponse
×
65
    {
66
        // Validate email format
67
        $rules    = $this->getValidationRules();
×
68
        $postData = $this->request->getPost();
×
69

70
        if (! $this->validateRequest($postData, $rules)) {
×
71
            return $this->handleValidationError('password-reset');
×
72
        }
73

74
        $email = $this->request->getPost('email');
×
75
        $user  = $this->provider->findByCredentials(['email' => $email]);
×
76

77
        if ($user !== null) {
×
78
            /** @var UserIdentityModel $identityModel */
79
            $identityModel = model(UserIdentityModel::class);
×
80

81
            // Delete any previous reset_password identities
82
            $identityModel->deleteIdentitiesByType($user, IdentityType::RESET_PASSWORD->value);
×
83

84
            // Generate the token and save it as an identity
85
            helper('text');
×
86
            $token = random_string('crypto', 20);
×
87

88
            $identityModel->insert([
×
89
                'user_id' => $user->id,
×
90
                'type'    => IdentityType::RESET_PASSWORD->value,
×
91
                'secret'  => $token,
×
92
                'expires' => Time::now()->addSeconds(setting('Auth.passwordResetLifetime'))->format('Y-m-d H:i:s'),
×
93
            ]);
×
94

95
            /** @var IncomingRequest $request */
96
            $request = service('request');
×
97

98
            $ipAddress = $request->getIPAddress();
×
99
            $userAgent = (string) $request->getUserAgent();
×
100
            $date      = Time::now()->toDateTimeString();
×
101

102
            // Send the user an email with the reset link
103
            helper('email');
×
104
            $emailService = emailer()->setFrom(setting('Email.fromEmail'), setting('Email.fromName') ?? '');
×
105
            $emailService->setTo($user->email);
×
106
            $emailService->setSubject(lang('Auth.passwordResetSubject'));
×
107
            $emailService->setMessage($this->view(setting('Auth.views')['password-reset-email'], [
×
108
                'token'     => $token,
×
109
                'ipAddress' => $ipAddress,
×
110
                'userAgent' => $userAgent,
×
111
                'date'      => $date,
×
112
                'user'      => $user,
×
113
            ]));
×
114

115
            if ($emailService->send(false) === false) {
×
116
                log_message('error', $emailService->printDebugger(['headers']));
×
117
            }
118

119
            // Clear the email
120
            $emailService->clear();
×
121

122
            // Record the attempt
123
            $this->recordAttempt($email, true, $user->id);
×
124
        } else {
125
            // Record failed attempt but don't reveal that user doesn't exist
126
            $this->recordAttempt($email, false);
×
127
        }
128

129
        // Always redirect to message view (don't reveal if user exists)
130
        return redirect()->route('password-reset-message');
×
131
    }
132

133
    /**
134
     * Shows the "check your email" message view.
135
     */
136
    public function messageView(): ResponseInterface
×
137
    {
138
        $content = $this->view(setting('Auth.views')['password-reset-message']);
×
139

140
        return $this->response->setBody($content);
×
141
    }
142

143
    /**
144
     * Displays the password reset form (with token from query string).
145
     */
146
    public function resetView(): ResponseInterface
×
147
    {
148
        $token = $this->request->getGet('token');
×
149

150
        /** @var UserIdentityModel $identityModel */
151
        $identityModel = model(UserIdentityModel::class);
×
152

153
        $identity = $identityModel->getIdentityBySecret(IdentityType::RESET_PASSWORD->value, $token);
×
154

155
        if ($identity === null) {
×
156
            return redirect()->route('password-reset-request')
×
157
                ->with('error', lang('Auth.passwordResetTokenInvalid'));
×
158
        }
159

160
        if (Time::now()->isAfter($identity->expires)) {
×
161
            // Delete expired token
162
            $identityModel->delete($identity->id);
×
163

164
            return redirect()->route('password-reset-request')
×
165
                ->with('error', lang('Auth.passwordResetTokenExpired'));
×
166
        }
167

168
        $content = $this->view(setting('Auth.views')['password-reset-form'], [
×
169
            'token' => $token,
×
170
        ]);
×
171

172
        return $this->response->setBody($content);
×
173
    }
174

175
    /**
176
     * Handles the password reset form submission.
177
     */
178
    public function resetAction(): RedirectResponse
×
179
    {
180
        $rules = [
×
181
            'token'            => 'required',
×
182
            'password'         => 'required|min_length[8]',
×
183
            'password_confirm' => 'required|matches[password]',
×
184
        ];
×
185

186
        $postData = $this->request->getPost();
×
187

188
        if (! $this->validateRequest($postData, $rules)) {
×
189
            $token = $this->request->getPost('token');
×
190

191
            return redirect()->to(site_url('password-reset/verify?token=' . $token))
×
192
                ->withInput()
×
193
                ->with('errors', $this->validator->getErrors());
×
194
        }
195

196
        $token = $this->request->getPost('token');
×
197

198
        /** @var UserIdentityModel $identityModel */
199
        $identityModel = model(UserIdentityModel::class);
×
200

201
        $identity = $identityModel->getIdentityBySecret(IdentityType::RESET_PASSWORD->value, $token);
×
202

203
        if ($identity === null) {
×
204
            return redirect()->route('password-reset-request')
×
205
                ->with('error', lang('Auth.passwordResetTokenInvalid'));
×
206
        }
207

208
        if (Time::now()->isAfter($identity->expires)) {
×
209
            $identityModel->delete($identity->id);
×
210

211
            return redirect()->route('password-reset-request')
×
212
                ->with('error', lang('Auth.passwordResetTokenExpired'));
×
213
        }
214

215
        // Find the user
216
        $user = $this->provider->findById($identity->user_id);
×
217

218
        if ($user === null) {
×
219
            return redirect()->route('password-reset-request')
×
220
                ->with('error', lang('Auth.passwordResetTokenInvalid'));
×
221
        }
222

223
        // Update the password
224
        $password = $this->request->getPost('password');
×
225
        $user->setPassword($password);
×
226

227
        /** @var UserModel $userModel */
228
        $userModel = model(UserModel::class);
×
229
        $userModel->save($user);
×
230

231
        // Delete the reset token identity
232
        $identityModel->delete($identity->id);
×
233

234
        Events::trigger('passwordReset', $user);
×
235

236
        return redirect()->route('login')
×
237
            ->with('message', lang('Auth.passwordResetSuccess'));
×
238
    }
239

240
    /**
241
     * Records a login attempt for the password reset flow.
242
     *
243
     * @param int|string|null $userId
244
     */
245
    private function recordAttempt(
×
246
        string $identifier,
247
        bool $success,
248
        $userId = null,
249
    ): void {
250
        /** @var LoginModel $loginModel */
251
        $loginModel = model(LoginModel::class);
×
252

253
        $loginModel->recordLoginAttempt(
×
254
            IdentityType::RESET_PASSWORD->value,
×
255
            $identifier,
×
256
            $success,
×
257
            $this->request->getIPAddress(),
×
258
            (string) $this->request->getUserAgent(),
×
259
            $userId,
×
260
        );
×
261
    }
262

263
    /**
264
     * Returns the rules that should be used for validation.
265
     *
266
     * @return         array<string, array<string, list<string>|string>>
267
     * @phpstan-return array<string, array<string, string|list<string>>>
268
     */
269
    protected function getValidationRules(): array
×
270
    {
271
        return [
×
272
            'email' => config('Auth')->emailValidationRules,
×
273
        ];
×
274
    }
275
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc