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

daycry / auth / 23341393117

20 Mar 2026 11:45AM UTC coverage: 64.989% (+1.2%) from 63.745%
23341393117

push

github

daycry
Merge branch 'development' of https://github.com/daycry/auth into development

4 of 4 new or added lines in 2 files covered. (100.0%)

315 existing lines in 13 files now uncovered.

3306 of 5087 relevant lines covered (64.99%)

47.03 hits per line

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

60.73
/src/Models/UserIdentityModel.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\RawSql;
17
use CodeIgniter\Exceptions\LogicException;
18
use CodeIgniter\I18n\Time;
19
use Daycry\Auth\Authentication\Authenticators\AccessToken;
20
use Daycry\Auth\Authentication\Authenticators\Session;
21
use Daycry\Auth\Entities\AccessToken as AccessTokenIdentity;
22
use Daycry\Auth\Entities\User;
23
use Daycry\Auth\Entities\UserIdentity;
24
use Daycry\Auth\Enums\IdentityType;
25
use Daycry\Auth\Exceptions\DatabaseException;
26
use Faker\Generator;
27

28
class UserIdentityModel extends BaseModel
29
{
30
    protected $table;
31
    protected $primaryKey     = 'id';
32
    protected $returnType     = UserIdentity::class;
33
    protected $useSoftDeletes = false;
34
    protected $allowedFields  = [
35
        'user_id',
36
        'name',
37
        'type',
38
        'secret',
39
        'secret2',
40
        'extra',
41
        'expires',
42
        'force_reset',
43
        'ignore_limits',
44
        'is_private',
45
        'ip_addresses',
46
        'last_used_at',
47
        'revoked_at',
48
    ];
49
    protected $useTimestamps = true;
50
    protected $createdField  = 'created_at';
51
    protected $updatedField  = 'updated_at';
52
    protected $deletedField  = 'deleted_at';
53

54
    protected function initialize(): void
351✔
55
    {
56
        parent::initialize();
351✔
57

58
        $this->table = $this->tables['identities'];
351✔
59
    }
60

61
    /**
62
     * Inserts a record
63
     *
64
     * @param array|object $data
65
     *
66
     * @throws DatabaseException
67
     */
68
    public function create($data): void
15✔
69
    {
70
        $this->disableDBDebug();
15✔
71

72
        $return = $this->insert($data);
15✔
73

74
        $this->checkQueryReturn($return);
15✔
75
    }
76

77
    /**
78
     * Creates a new identity for this user with an email/password
79
     * combination.
80
     *
81
     * @phpstan-param array{email: string, password: string} $credentials
82
     */
83
    public function createEmailIdentity(User $user, array $credentials): void
122✔
84
    {
85
        $this->checkUserId($user);
122✔
86

87
        /** @var Passwords $passwords */
88
        $passwords = service('passwords');
122✔
89

90
        $return = $this->insert([
122✔
91
            'user_id' => $user->id,
122✔
92
            'type'    => Session::ID_TYPE_EMAIL_PASSWORD,
122✔
93
            'secret'  => $credentials['email'],
122✔
94
            'secret2' => $passwords->hash($credentials['password']),
122✔
95
        ]);
122✔
96

97
        $this->checkQueryReturn($return);
122✔
98
    }
99

100
    private function checkUserId(User $user): void
293✔
101
    {
102
        if ($user->id === null) {
293✔
103
            throw new LogicException(
1✔
104
                '"$user->id" is null. You should not use the incomplete User object.',
1✔
105
            );
1✔
106
        }
107
    }
108

109
    /**
110
     * Create an identity with 6 digits code for auth action
111
     *
112
     * @param         callable                                         $codeGenerator generate secret code
113
     * @phpstan-param array{type: string, name: string, extra: string} $data
114
     *
115
     * @return string secret
116
     */
117
    public function createCodeIdentity(
15✔
118
        User $user,
119
        array $data,
120
        callable $codeGenerator,
121
    ): string {
122
        $this->checkUserId($user);
15✔
123

124
        helper('text');
15✔
125

126
        // Create an identity for the action
127
        $maxTry          = 5;
15✔
128
        $data['user_id'] = $user->id;
15✔
129

130
        while (true) {
15✔
131
            $data['secret'] = $codeGenerator();
15✔
132

133
            try {
134
                $this->create($data);
15✔
135

136
                break;
15✔
137
            } catch (DatabaseException $e) {
×
138
                $maxTry--;
×
139

140
                if ($maxTry === 0) {
×
141
                    throw $e;
×
142
                }
143
            }
144
        }
145

146
        return $data['secret'];
15✔
147
    }
148

149
    /**
150
     * Generates a new personal access token for the user.
151
     *
152
     * @deprecated Use AccessTokenRepository::generateAccessToken() instead.
153
     *
154
     * @param string       $name   Token name
155
     * @param list<string> $scopes Permissions the token grants
156
     */
157
    public function generateAccessToken(User $user, string $name, array $scopes = ['*']): AccessTokenIdentity
10✔
158
    {
159
        $this->checkUserId($user);
10✔
160

161
        helper('text');
10✔
162

163
        $return = $this->insert([
10✔
164
            'type'    => AccessToken::ID_TYPE_ACCESS_TOKEN,
10✔
165
            'user_id' => $user->id,
10✔
166
            'name'    => $name,
10✔
167
            'secret'  => hash('sha256', $rawToken = random_string('crypto', 64)),
10✔
168
            'extra'   => json_encode($scopes, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
10✔
169
        ]);
10✔
170

171
        $this->checkQueryReturn($return);
10✔
172

173
        /** @var AccessTokenIdentity $token */
174
        $token = $this
10✔
175
            ->asObject(AccessTokenIdentity::class)
10✔
176
            ->find($this->getInsertID());
10✔
177

178
        $token->raw_token = $rawToken;
10✔
179

180
        return $token;
10✔
181
    }
182

183
    /**
184
     * @deprecated Use AccessTokenRepository::getAccessTokenByRawToken() instead.
185
     */
186
    public function getAccessTokenByRawToken(string $rawToken): ?AccessTokenIdentity
9✔
187
    {
188
        return $this
9✔
189
            ->where('type', AccessToken::ID_TYPE_ACCESS_TOKEN)
9✔
190
            ->where('secret', hash('sha256', $rawToken))
9✔
191
            ->where('revoked_at', null)
9✔
192
            ->asObject(AccessTokenIdentity::class)
9✔
193
            ->first();
9✔
194
    }
195

196
    /**
197
     * @deprecated Use AccessTokenRepository::getAccessToken() instead.
198
     */
199
    public function getAccessToken(User $user, string $rawToken): ?AccessTokenIdentity
2✔
200
    {
201
        $this->checkUserId($user);
2✔
202

203
        return $this->where('user_id', $user->id)
2✔
204
            ->where('type', AccessToken::ID_TYPE_ACCESS_TOKEN)
2✔
205
            ->where('secret', hash('sha256', $rawToken))
2✔
206
            ->asObject(AccessTokenIdentity::class)
2✔
207
            ->first();
2✔
208
    }
209

210
    /**
211
     * Given the ID, returns the given access token.
212
     *
213
     * @deprecated Use AccessTokenRepository::getAccessTokenById() instead.
214
     *
215
     * @param int|string $id
216
     */
UNCOV
217
    public function getAccessTokenById($id, User $user): ?AccessTokenIdentity
×
218
    {
UNCOV
219
        $this->checkUserId($user);
×
220

221
        return $this->where('user_id', $user->id)
×
UNCOV
222
            ->where('type', AccessToken::ID_TYPE_ACCESS_TOKEN)
×
223
            ->where('id', $id)
×
UNCOV
224
            ->asObject(AccessTokenIdentity::class)
×
225
            ->first();
×
226
    }
227

228
    /**
229
     * @deprecated Use AccessTokenRepository::getAllAccessTokens() instead.
230
     *
231
     * @return list<AccessTokenIdentity>
232
     */
UNCOV
233
    public function getAllAccessToken(User $user): array
×
234
    {
UNCOV
235
        $this->checkUserId($user);
×
236

UNCOV
237
        return $this
×
238
            ->where('user_id', $user->id)
×
239
            ->where('type', AccessToken::ID_TYPE_ACCESS_TOKEN)
×
UNCOV
240
            ->orderBy($this->primaryKey)
×
UNCOV
241
            ->asObject(AccessTokenIdentity::class)
×
242
            ->findAll();
×
243
    }
244

245
    /**
246
     * Used by 'magic-link'.
247
     */
UNCOV
248
    public function getIdentityBySecret(string $type, ?string $secret): ?UserIdentity
×
249
    {
UNCOV
250
        if ($secret === null) {
×
UNCOV
251
            return null;
×
252
        }
253

UNCOV
254
        return $this->where('type', $type)
×
UNCOV
255
            ->where('secret', $secret)
×
UNCOV
256
            ->first();
×
257
    }
258

259
    /**
260
     * Returns all identities.
261
     *
262
     * @return list<UserIdentity>
263
     */
264
    public function getIdentities(User $user): array
293✔
265
    {
266
        $this->checkUserId($user);
293✔
267

268
        return $this->where('user_id', $user->id)->orderBy($this->primaryKey)->findAll();
293✔
269
    }
270

271
    /**
272
     * @param list<int>|list<string> $userIds
273
     *
274
     * @return list<UserIdentity>
275
     */
276
    public function getIdentitiesByUserIds(array $userIds): array
2✔
277
    {
278
        return $this->whereIn('user_id', $userIds)->orderBy($this->primaryKey)->findAll();
2✔
279
    }
280

281
    /**
282
     * Returns the first identity of the type.
283
     */
284
    public function getIdentityByType(User $user, string $type): ?UserIdentity
97✔
285
    {
286
        $this->checkUserId($user);
97✔
287

288
        return $this->where('user_id', $user->id)
97✔
289
            ->where('type', $type)
97✔
290
            ->orderBy($this->primaryKey)
97✔
291
            ->first();
97✔
292
    }
293

294
    /**
295
     * Returns all identities for the specific types.
296
     *
297
     * @param list<string> $types
298
     *
299
     * @return list<UserIdentity>
300
     */
301
    public function getIdentitiesByTypes(User $user, array $types): array
5✔
302
    {
303
        $this->checkUserId($user);
5✔
304

305
        if ($types === []) {
5✔
306
            return [];
3✔
307
        }
308

309
        return $this->where('user_id', $user->id)
2✔
310
            ->whereIn('type', $types)
2✔
311
            ->orderBy($this->primaryKey)
2✔
312
            ->findAll();
2✔
313
    }
314

315
    /**
316
     * Update the last used at date for an identity record.
317
     */
318
    public function touchIdentity(UserIdentity $identity): void
22✔
319
    {
320
        $identity->last_used_at = Time::now()->format('Y-m-d H:i:s');
22✔
321

322
        $return = $this->save($identity);
22✔
323

324
        $this->checkQueryReturn($return);
22✔
325
    }
326

327
    public function deleteIdentitiesByType(User $user, string $type): void
38✔
328
    {
329
        $this->checkUserId($user);
38✔
330

331
        $return = $this->where('user_id', $user->id)
38✔
332
            ->where('type', $type)
38✔
333
            ->delete();
38✔
334

335
        $this->checkQueryReturn($return);
38✔
336
    }
337

338
    /**
339
     * Delete any access tokens for the given raw token.
340
     *
341
     * @deprecated Use AccessTokenRepository::deleteAccessToken() or softRevokeAccessToken() instead.
342
     */
UNCOV
343
    public function revokeAccessToken(User $user, string $rawToken): void
×
344
    {
UNCOV
345
        $this->checkUserId($user);
×
346

UNCOV
347
        $return = $this->where('user_id', $user->id)
×
348
            ->where('type', AccessToken::ID_TYPE_ACCESS_TOKEN)
×
349
            ->where('secret', hash('sha256', $rawToken))
×
350
            ->delete();
×
351

UNCOV
352
        $this->checkQueryReturn($return);
×
353
    }
354

355
    /**
356
     * Delete any access tokens for the given secret token.
357
     *
358
     * @deprecated Use AccessTokenRepository::deleteAccessTokenBySecret() or softRevokeAccessTokenBySecret() instead.
359
     */
UNCOV
360
    public function revokeAccessTokenBySecret(User $user, string $secretToken): void
×
361
    {
UNCOV
362
        $this->checkUserId($user);
×
363

364
        $return = $this->where('user_id', $user->id)
×
365
            ->where('type', AccessToken::ID_TYPE_ACCESS_TOKEN)
×
UNCOV
366
            ->where('secret', $secretToken)
×
367
            ->delete();
×
368

UNCOV
369
        $this->checkQueryReturn($return);
×
370
    }
371

372
    /**
373
     * Revokes all access tokens for this user.
374
     *
375
     * @deprecated Use AccessTokenRepository::deleteAllAccessTokens() or softRevokeAllAccessTokens() instead.
376
     */
377
    public function revokeAllAccessToken(User $user): void
×
378
    {
UNCOV
379
        $this->checkUserId($user);
×
380

UNCOV
381
        $return = $this->where('user_id', $user->id)
×
UNCOV
382
            ->where('type', AccessToken::ID_TYPE_ACCESS_TOKEN)
×
UNCOV
383
            ->delete();
×
384

UNCOV
385
        $this->checkQueryReturn($return);
×
386
    }
387

388
    /**
389
     * Soft-revoke all non-revoked identities for a user and type in a single UPDATE.
390
     */
391
    public function revokeIdentitiesByUserAndType(int $userId, string $type): void
×
392
    {
393
        $this->where('user_id', $userId)
×
394
            ->where('type', $type)
×
395
            ->where('revoked_at', null)
×
396
            ->set('revoked_at', Time::now()->format('Y-m-d H:i:s'))
×
UNCOV
397
            ->update();
×
398
    }
399

400
    /**
401
     * Soft-revoke an identity by its primary key (sets revoked_at).
402
     */
UNCOV
403
    public function revokeIdentityById(int $id): void
×
404
    {
405
        $this->where('id', $id)
×
UNCOV
406
            ->set('revoked_at', Time::now()->format('Y-m-d H:i:s'))
×
407
            ->update();
×
408
    }
409

410
    /**
411
     * Stores a new JWT refresh token for the given user.
412
     *
413
     * The raw token is hashed (SHA-256) before storage.
414
     *
415
     * @deprecated Use JwtTokenRepository::createRefreshToken() instead.
416
     *
417
     * @param int    $userId    User primary key
418
     * @param string $rawToken  The raw (unhashed) token to store
419
     * @param string $expiresAt Datetime string 'Y-m-d H:i:s'
420
     */
UNCOV
421
    public function createJwtRefreshToken(int $userId, string $rawToken, string $expiresAt): void
×
422
    {
423
        $this->insert([
×
424
            'user_id' => $userId,
×
425
            'type'    => IdentityType::JWT_REFRESH->value,
×
UNCOV
426
            'secret'  => hash('sha256', $rawToken),
×
427
            'expires' => $expiresAt,
×
UNCOV
428
        ]);
×
429
    }
430

431
    /**
432
     * Finds a valid (non-expired, non-revoked) JWT refresh token.
433
     *
434
     * @deprecated Use JwtTokenRepository::getRefreshToken() instead.
435
     *
436
     * @param int    $userId   User primary key
437
     * @param string $rawToken The raw (unhashed) token
438
     */
UNCOV
439
    public function getJwtRefreshToken(int $userId, string $rawToken): ?UserIdentity
×
440
    {
UNCOV
441
        return $this->where('user_id', $userId)
×
UNCOV
442
            ->where('type', IdentityType::JWT_REFRESH->value)
×
UNCOV
443
            ->where('secret', hash('sha256', $rawToken))
×
UNCOV
444
            ->where('revoked_at', null)
×
UNCOV
445
            ->where('expires >', Time::now()->format('Y-m-d H:i:s'))
×
UNCOV
446
            ->first();
×
447
    }
448

449
    /**
450
     * Force password reset for multiple users.
451
     *
452
     * @param list<int>|list<string> $userIds
453
     */
UNCOV
454
    public function forceMultiplePasswordReset(array $userIds): void
×
455
    {
UNCOV
456
        $this->where(['type' => Session::ID_TYPE_EMAIL_PASSWORD, 'force_reset' => 0]);
×
UNCOV
457
        $this->whereIn('user_id', $userIds);
×
UNCOV
458
        $this->set('force_reset', 1);
×
UNCOV
459
        $return = $this->update();
×
460

UNCOV
461
        $this->checkQueryReturn($return);
×
462
    }
463

464
    /**
465
     * Force global password reset.
466
     * This is useful for enforcing a password reset
467
     * for ALL users in case of a security breach.
468
     */
469
    public function forceGlobalPasswordReset(): void
1✔
470
    {
471
        $whereFilter = [
1✔
472
            'type'        => Session::ID_TYPE_EMAIL_PASSWORD,
1✔
473
            'force_reset' => 0,
1✔
474
        ];
1✔
475
        $this->where($whereFilter);
1✔
476
        $this->set('force_reset', 1);
1✔
477
        $return = $this->update();
1✔
478

479
        $this->checkQueryReturn($return);
1✔
480
    }
481

482
    /**
483
     * Override the Model's `update()` method.
484
     * Throws an Exception when it fails.
485
     *
486
     * @param array|int|list<int|string>|RawSql|string|null $id
487
     * @param array|object|null                             $data
488
     *
489
     * @return true if the update is successful
490
     *
491
     * @throws ValidationException
492
     */
493
    public function update($id = null, $data = null): bool
105✔
494
    {
495
        $result = parent::update($id, $data);
105✔
496

497
        $this->checkQueryReturn($result);
67✔
498

499
        return true;
67✔
500
    }
501

502
    public function fake(Generator &$faker): UserIdentity
6✔
503
    {
504
        return new UserIdentity([
6✔
505
            'user_id'      => fake(UserModel::class)->id,
6✔
506
            'type'         => Session::ID_TYPE_EMAIL_PASSWORD,
6✔
507
            'name'         => null,
6✔
508
            'secret'       => $faker->unique()->email(),
6✔
509
            'secret2'      => password_hash('secret', PASSWORD_DEFAULT),
6✔
510
            'expires'      => null,
6✔
511
            'extra'        => null,
6✔
512
            'force_reset'  => false,
6✔
513
            'last_used_at' => null,
6✔
514
        ]);
6✔
515
    }
516
}
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