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

daycry / auth / 21031260116

15 Jan 2026 11:02AM UTC coverage: 66.786% (+0.2%) from 66.555%
21031260116

push

github

daycry
Refactor Auth config for improved organization

Reorganized the Auth configuration file by grouping related settings into logical sections such as logging, rate limiting, authentication, user/registration, password/security, OAuth, views/URLs, and discovery/cron. This improves readability and maintainability without changing any functional behavior.

2246 of 3363 relevant lines covered (66.79%)

33.3 hits per line

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

94.93
/src/Models/UserModel.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\Models;
15

16
use CodeIgniter\Database\Exceptions\DataException;
17
use CodeIgniter\I18n\Time;
18
use Daycry\Auth\Authentication\Authenticators\Session;
19
use Daycry\Auth\Entities\User;
20
use Daycry\Auth\Entities\UserIdentity;
21
use Daycry\Auth\Exceptions\InvalidArgumentException;
22
use Daycry\Auth\Interfaces\UserProviderInterface;
23
use Faker\Generator;
24

25
/**
26
 * @phpstan-consistent-constructor
27
 */
28
class UserModel extends BaseModel implements UserProviderInterface
29
{
30
    protected $table;
31
    protected $primaryKey     = 'id';
32
    protected $returnType     = User::class;
33
    protected $useSoftDeletes = false;
34
    protected $allowedFields  = [
35
        'username',
36
        'status',
37
        'status_message',
38
        'active',
39
        'last_active',
40
    ];
41
    protected $useTimestamps = true;
42
    protected $createdField  = 'created_at';
43
    protected $updatedField  = 'updated_at';
44
    protected $deletedField  = 'deleted_at';
45
    protected $afterFind     = ['fetchIdentities'];
46
    protected $afterInsert   = ['saveEmailIdentity'];
47
    protected $afterUpdate   = ['saveEmailIdentity'];
48

49
    /**
50
     * Whether identity records should be included
51
     * when user records are fetched from the database.
52
     */
53
    protected bool $fetchIdentities = false;
54

55
    /**
56
     * Save the User for afterInsert and afterUpdate
57
     */
58
    protected ?User $tempUser = null;
59

60
    protected function initialize(): void
61
    {
62
        parent::initialize();
202✔
63

64
        $this->table = $this->tables['users'];
202✔
65
    }
66

67
    /**
68
     * Mark the next find* query to include identities
69
     *
70
     * @return $this
71
     */
72
    public function withIdentities(): self
73
    {
74
        $this->fetchIdentities = true;
4✔
75

76
        return $this;
4✔
77
    }
78

79
    /**
80
     * Populates identities for all records
81
     * returned from a find* method. Called
82
     * automatically when $this->fetchIdentities == true
83
     *
84
     * Model event callback called by `afterFind`.
85
     */
86
    protected function fetchIdentities(array $data): array
87
    {
88
        if (! $this->fetchIdentities || empty($data['data'])) {
154✔
89
            return $data;
154✔
90
        }
91

92
        $userIds = $data['singleton']
2✔
93
            ? [$data['data']->id]
1✔
94
            : array_column($data['data'], 'id');
1✔
95

96
        if ($userIds === []) {
2✔
97
            return $data;
×
98
        }
99

100
        /** @var UserIdentityModel $identityModel */
101
        $identityModel = model(UserIdentityModel::class);
2✔
102

103
        // Get our identities for all users
104
        $identities = $identityModel->getIdentitiesByUserIds($userIds);
2✔
105

106
        if (empty($identities)) {
2✔
107
            return $data;
×
108
        }
109

110
        $mappedUsers = $this->assignIdentities($data, $identities);
2✔
111

112
        $data['data'] = $data['singleton'] ? $mappedUsers[array_column($data, 'id')[0]] : $mappedUsers;
2✔
113

114
        return $data;
2✔
115
    }
116

117
    /**
118
     * Map our users by ID to make assigning simpler
119
     *
120
     * @param array              $data       Event $data
121
     * @param list<UserIdentity> $identities
122
     *
123
     * @return         list<User>              UserId => User object
124
     * @phpstan-return array<int|string, User> UserId => User object
125
     */
126
    private function assignIdentities(array $data, array $identities): array
127
    {
128
        $mappedUsers    = [];
2✔
129
        $userIdentities = [];
2✔
130

131
        $users = $data['singleton'] ? [$data['data']] : $data['data'];
2✔
132

133
        foreach ($users as $user) {
2✔
134
            $mappedUsers[$user->id] = $user;
2✔
135
        }
136
        unset($users);
2✔
137

138
        // Now group the identities by user
139
        foreach ($identities as $identity) {
2✔
140
            $userIdentities[$identity->user_id][] = $identity;
2✔
141
        }
142
        unset($identities);
2✔
143

144
        // Now assign the identities to the user
145
        foreach ($userIdentities as $userId => $identityArray) {
2✔
146
            $mappedUsers[$userId]->identities = $identityArray;
2✔
147
        }
148
        unset($userIdentities);
2✔
149

150
        return $mappedUsers;
2✔
151
    }
152

153
    /**
154
     * Activate a User.
155
     */
156
    public function activate(User $user): void
157
    {
158
        $user->active = true;
×
159

160
        $this->save($user);
×
161
    }
162

163
    /**
164
     * Override the BaseModel's `insert()` method.
165
     * If you pass User object, also inserts Email Identity.
166
     *
167
     * @param array|User $data
168
     *
169
     * @return int|string|true Insert ID if $returnID is true
170
     *
171
     * @throws ValidationException
172
     */
173
    public function insert($data = null, bool $returnID = true)
174
    {
175
        // Clone User object for not changing the passed object.
176
        $this->tempUser = $data instanceof User ? clone $data : null;
156✔
177

178
        $result = parent::insert($data, $returnID);
156✔
179

180
        $this->checkQueryReturn($result);
156✔
181

182
        return $returnID ? $this->insertID : $result;
156✔
183
    }
184

185
    /**
186
     * Override the BaseModel's `update()` method.
187
     * If you pass User object, also updates Email Identity.
188
     *
189
     * @param array|int|string|null $id
190
     * @param array|User            $data
191
     *
192
     * @return true if the update is successful
193
     *
194
     * @throws ValidationException
195
     */
196
    public function update($id = null, $data = null): bool
197
    {
198
        // Clone User object for not changing the passed object.
199
        $this->tempUser = $data instanceof User ? clone $data : null;
29✔
200

201
        try {
202
            /** @throws DataException */
203
            $result = parent::update($id, $data);
29✔
204
        } catch (DataException $e) {
10✔
205
            // When $data is an array.
206
            if ($this->tempUser === null) {
10✔
207
                throw $e;
1✔
208
            }
209

210
            $messages = [
9✔
211
                lang('Database.emptyDataset', ['update']),
9✔
212
            ];
9✔
213

214
            if (in_array($e->getMessage(), $messages, true)) {
9✔
215
                $this->saveEmailIdentity([]);
9✔
216

217
                return true;
9✔
218
            }
219

220
            throw $e;
×
221
        }
222

223
        $this->checkQueryReturn($result);
22✔
224

225
        return true;
22✔
226
    }
227

228
    /**
229
     * Override the BaseModel's `save()` method.
230
     * If you pass User object, also updates Email Identity.
231
     *
232
     * @param array|User $data
233
     *
234
     * @return true if the save is successful
235
     *
236
     * @throws ValidationException
237
     */
238
    public function save($data): bool
239
    {
240
        $result = parent::save($data);
33✔
241

242
        $this->checkQueryReturn($result);
32✔
243

244
        return true;
32✔
245
    }
246

247
    /**
248
     * Save Email Identity
249
     *
250
     * Model event callback called by `afterInsert` and `afterUpdate`.
251
     */
252
    protected function saveEmailIdentity(array $data): array
253
    {
254
        // If insert()/update() gets an array data, do nothing.
255
        if ($this->tempUser === null) {
156✔
256
            return $data;
7✔
257
        }
258

259
        /** @var User $user */
260
        $user = $this->tempUser;
155✔
261

262
        if ($user->id === null) {
155✔
263
            $user->id = $data['id'] ?? $this->db->insertID();
150✔
264
        }
265

266
        $email        = $user->getEmail();
155✔
267
        $password     = $user->getPassword();
155✔
268
        $passwordHash = $user->getPasswordHash();
155✔
269

270
        if (($email === null || $email === '' || $email === '0') && ($password === null || $password === '' || $password === '0') && ($passwordHash === null || $passwordHash === '' || $passwordHash === '0')) {
155✔
271
            $this->tempUser = null;
144✔
272

273
            return $data;
144✔
274
        }
275

276
        /** @var UserIdentityModel $identityModel */
277
        $identityModel = model(UserIdentityModel::class);
21✔
278
        $identity      = $identityModel->getIdentityByType($user, Session::ID_TYPE_EMAIL_PASSWORD);
21✔
279

280
        if ($identity === null) {
21✔
281
            $identityModel->createEmailIdentity($user, [
15✔
282
                'email'    => $email,
15✔
283
                'password' => '',
15✔
284
            ]);
15✔
285

286
            $identity = $identityModel->getIdentityByType($user, Session::ID_TYPE_EMAIL_PASSWORD);
15✔
287
        }
288

289
        if ($email !== null && $email !== '' && $email !== '0') {
21✔
290
            $identity->secret = $email;
21✔
291
        }
292

293
        if ($password !== null && $password !== '' && $password !== '0') {
21✔
294
            $identity->secret2 = service('passwords')->hash($password);
14✔
295
        }
296

297
        if ($passwordHash !== null && $passwordHash !== '' && $passwordHash !== '0' && ($password === null || $password === '' || $password === '0')) {
21✔
298
            $identity->secret2 = $passwordHash;
10✔
299
        }
300

301
        try {
302
            /** @throws DataException */
303
            $identityModel->save($identity);
21✔
304
        } catch (DataException $e) {
4✔
305
            // There may be no data to update.
306
            $messages = [
4✔
307
                lang('Database.emptyDataset', ['insert']),
4✔
308
                lang('Database.emptyDataset', ['update']),
4✔
309
            ];
4✔
310
            if (! in_array($e->getMessage(), $messages, true)) {
4✔
311
                throw $e;
×
312
            }
313
        }
314

315
        $this->tempUser = null;
21✔
316

317
        return $data;
21✔
318
    }
319

320
    /**
321
     * Adds a user to the default group.
322
     * Used during registration.
323
     */
324
    public function addToDefaultGroup(User $user): void
325
    {
326
        $defaultGroup = setting('Auth.defaultGroup');
1✔
327

328
        $rows = model(GroupModel::class)->findAll();
1✔
329

330
        $allowedGroups = array_column($rows, 'name');
1✔
331

332
        if (empty($defaultGroup) || ! in_array($defaultGroup, $allowedGroups, true)) {
1✔
333
            throw new InvalidArgumentException(lang('Auth.unknownGroup', [$defaultGroup ?? '--not found--']));
×
334
        }
335

336
        $user->addGroup($defaultGroup);
1✔
337
    }
338

339
    public function fake(Generator &$faker): User
340
    {
341
        return new User([
143✔
342
            'username' => $faker->unique()->userName(),
143✔
343
            'active'   => true,
143✔
344
        ]);
143✔
345
    }
346

347
    /**
348
     * Locates a User object by ID.
349
     *
350
     * @param int|string $id
351
     */
352
    public function findById($id): ?User
353
    {
354
        return $this->find($id);
54✔
355
    }
356

357
    /**
358
     * Locate a User object by the given credentials.
359
     *
360
     * @param array<string, string> $credentials
361
     */
362
    public function findByCredentials(array $credentials): ?User
363
    {
364
        // Email is stored in an identity so remove that here
365
        $email = $credentials['email'] ?? null;
38✔
366
        unset($credentials['email']);
38✔
367

368
        if ($email === null && $credentials === []) {
38✔
369
            return null;
1✔
370
        }
371

372
        // any of the credentials used should be case-insensitive
373
        foreach ($credentials as $key => $value) {
38✔
374
            $this->where(
2✔
375
                'LOWER(' . $this->db->protectIdentifiers($this->table . ".{$key}") . ')',
2✔
376
                strtolower($value),
2✔
377
            );
2✔
378
        }
379

380
        if ($email !== null) {
38✔
381
            /** @var array<string, int|string|null>|null $data */
382
            $data = $this->select(
36✔
383
                sprintf('%1$s.*, %2$s.secret as email, %2$s.secret2 as password_hash', $this->table, $this->tables['identities']),
36✔
384
            )
36✔
385
                ->join($this->tables['identities'], sprintf('%1$s.user_id = %2$s.id', $this->tables['identities'], $this->table))
36✔
386
                ->where($this->tables['identities'] . '.type', Session::ID_TYPE_EMAIL_PASSWORD)
36✔
387
                ->where(
36✔
388
                    'LOWER(' . $this->db->protectIdentifiers($this->tables['identities'] . '.secret') . ')',
36✔
389
                    strtolower($email),
36✔
390
                )
36✔
391
                ->asArray()
36✔
392
                ->first();
36✔
393

394
            if ($data === null) {
36✔
395
                return null;
9✔
396
            }
397

398
            $email = $data['email'];
27✔
399
            unset($data['email']);
27✔
400
            $password_hash = $data['password_hash'];
27✔
401
            unset($data['password_hash']);
27✔
402

403
            $user                = new User($data);
27✔
404
            $user->email         = $email;
27✔
405
            $user->password_hash = $password_hash;
27✔
406
            $user->syncOriginal();
27✔
407

408
            return $user;
27✔
409
        }
410

411
        return $this->first();
2✔
412
    }
413

414
    /**
415
     * Updates the user's last active date.
416
     */
417
    public function updateActiveDate(User $user): void
418
    {
419
        assert($user->last_active instanceof Time);
420

421
        // Safe date string for database
422
        $last_active = $user->last_active->format('Y-m-d H:i:s');
5✔
423

424
        $this->builder()
5✔
425
            ->set('last_active', $last_active)
5✔
426
            ->where('id', $user->id)
5✔
427
            ->update();
5✔
428
    }
429
}
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