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

daycry / auth / 22527658769

28 Feb 2026 07:41PM UTC coverage: 63.267% (-3.6%) from 66.864%
22527658769

push

github

web-flow
Merge pull request #36 from daycry/development

Implement TOTP 2FA, JWT auth, device session tracking, and docs overhaul

465 of 1168 new or added lines in 52 files covered. (39.81%)

129 existing lines in 46 files now uncovered.

3064 of 4843 relevant lines covered (63.27%)

41.53 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
236
        return redirect()->route('login')
×
NEW
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
     */
NEW
245
    private function recordAttempt(
×
246
        string $identifier,
247
        bool $success,
248
        $userId = null,
249
    ): void {
250
        /** @var LoginModel $loginModel */
NEW
251
        $loginModel = model(LoginModel::class);
×
252

NEW
253
        $loginModel->recordLoginAttempt(
×
NEW
254
            IdentityType::RESET_PASSWORD->value,
×
NEW
255
            $identifier,
×
NEW
256
            $success,
×
NEW
257
            $this->request->getIPAddress(),
×
NEW
258
            (string) $this->request->getUserAgent(),
×
NEW
259
            $userId,
×
NEW
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
     */
NEW
269
    protected function getValidationRules(): array
×
270
    {
NEW
271
        return [
×
NEW
272
            'email' => config('Auth')->emailValidationRules,
×
NEW
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