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

daycry / auth / 25518434194

07 May 2026 07:49PM UTC coverage: 58.608% (-6.4%) from 64.989%
25518434194

push

github

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

Implement security enhancements and new account features

277 of 1030 new or added lines in 55 files covered. (26.89%)

11 existing lines in 6 files now uncovered.

3544 of 6047 relevant lines covered (58.61%)

47.97 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\Exceptions\RuntimeException;
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\Libraries\TokenEmailSender;
23
use Daycry\Auth\Models\UserIdentityModel;
24
use Daycry\Auth\Models\UserModel;
25
use Daycry\Auth\Services\AuditLogger;
26
use Daycry\Auth\Services\PasswordChangeRecorder;
27

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

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

46
        $this->provider = new $providerClass();
×
47
    }
48

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

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

60
        return $this->response->setBody($content);
×
61
    }
62

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

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

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

79
        if ($user !== null) {
×
80
            $sender = new TokenEmailSender();
×
81

82
            try {
83
                $sender->sendTokenEmail(
×
84
                    $user,
×
85
                    IdentityType::RESET_PASSWORD->value,
×
86
                    setting('AuthSecurity.passwordResetLifetime'),
×
87
                    lang('Auth.passwordResetSubject'),
×
88
                    setting('Auth.views')['password-reset-email'],
×
89
                    ['user' => $user],
×
90
                );
×
91
            } catch (RuntimeException $e) {
×
92
                log_message('error', $e->getMessage());
×
93
            }
94

95
            // Record the attempt
96
            $this->recordLoginAttempt(IdentityType::RESET_PASSWORD->value, $email, true, $user->id);
×
97
        } else {
98
            // Record failed attempt but don't reveal that user doesn't exist
99
            $this->recordLoginAttempt(IdentityType::RESET_PASSWORD->value, $email, false);
×
100
        }
101

102
        // Always redirect to message view (don't reveal if user exists)
103
        return redirect()->route('password-reset-message');
×
104
    }
105

106
    /**
107
     * Shows the "check your email" message view.
108
     */
109
    public function messageView(): ResponseInterface
×
110
    {
111
        $content = $this->view(setting('Auth.views')['password-reset-message']);
×
112

113
        return $this->response->setBody($content);
×
114
    }
115

116
    /**
117
     * Displays the password reset form (with token from query string).
118
     */
119
    public function resetView(): ResponseInterface
×
120
    {
121
        $token = $this->request->getGet('token');
×
122

123
        /** @var UserIdentityModel $identityModel */
124
        $identityModel = model(UserIdentityModel::class);
×
125

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

128
        if ($identity === null) {
×
129
            return redirect()->route('password-reset-request')
×
130
                ->with('error', lang('Auth.passwordResetTokenInvalid'));
×
131
        }
132

133
        if (Time::now()->isAfter($identity->expires)) {
×
134
            // Delete expired token
135
            $identityModel->delete($identity->id);
×
136

137
            return redirect()->route('password-reset-request')
×
138
                ->with('error', lang('Auth.passwordResetTokenExpired'));
×
139
        }
140

141
        $content = $this->view(setting('Auth.views')['password-reset-form'], [
×
142
            'token' => $token,
×
143
        ]);
×
144

145
        return $this->response->setBody($content);
×
146
    }
147

148
    /**
149
     * Handles the password reset form submission.
150
     */
151
    public function resetAction(): RedirectResponse
×
152
    {
153
        $rules = [
×
154
            'token'            => 'required',
×
155
            'password'         => 'required|min_length[8]',
×
156
            'password_confirm' => 'required|matches[password]',
×
157
        ];
×
158

159
        $postData = $this->request->getPost();
×
160

161
        if (! $this->validateRequest($postData, $rules)) {
×
162
            $token = $this->request->getPost('token');
×
163

164
            return redirect()->to(site_url('password-reset/verify?token=' . $token))
×
165
                ->withInput()
×
166
                ->with('errors', $this->validator->getErrors());
×
167
        }
168

169
        $token = $this->request->getPost('token');
×
170

171
        /** @var UserIdentityModel $identityModel */
172
        $identityModel = model(UserIdentityModel::class);
×
173

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

176
        if ($identity === null) {
×
177
            return redirect()->route('password-reset-request')
×
178
                ->with('error', lang('Auth.passwordResetTokenInvalid'));
×
179
        }
180

181
        if (Time::now()->isAfter($identity->expires)) {
×
182
            $identityModel->delete($identity->id);
×
183

184
            return redirect()->route('password-reset-request')
×
185
                ->with('error', lang('Auth.passwordResetTokenExpired'));
×
186
        }
187

188
        // Find the user
189
        $user = $this->provider->findById($identity->user_id);
×
190

191
        if ($user === null) {
×
192
            return redirect()->route('password-reset-request')
×
193
                ->with('error', lang('Auth.passwordResetTokenInvalid'));
×
194
        }
195

196
        // Capture the previous hash *before* we replace it — needed for
197
        // password-history bookkeeping.
NEW
198
        $previousHash = $user->password_hash ?? null;
×
199

200
        // Update the password
201
        $password = $this->request->getPost('password');
×
202
        $user->setPassword($password);
×
203

204
        /** @var UserModel $userModel */
205
        $userModel = model(UserModel::class);
×
206
        $userModel->save($user);
×
207

NEW
208
        (new PasswordChangeRecorder())->record($user, $previousHash);
×
209

210
        // Delete the reset token identity
211
        $identityModel->delete($identity->id);
×
212

NEW
213
        (new AuditLogger())->record(AuditLogger::EVENT_PASSWORD_RESET, (int) $user->id);
×
214

UNCOV
215
        Events::trigger('passwordReset', $user);
×
216

217
        return redirect()->route('login')
×
218
            ->with('message', lang('Auth.passwordResetSuccess'));
×
219
    }
220

221
    /**
222
     * Returns the rules that should be used for validation.
223
     *
224
     * @return         array<string, array<string, list<string>|string>>
225
     * @phpstan-return array<string, array<string, string|list<string>>>
226
     */
227
    protected function getValidationRules(): array
×
228
    {
229
        return [
×
230
            'email' => config('Auth')->emailValidationRules,
×
231
        ];
×
232
    }
233
}
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