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

daycry / auth / 25507696361

07 May 2026 03:28PM UTC coverage: 58.608% (-6.4%) from 64.989%
25507696361

push

github

daycry
Regenerate PHPStan baseline for the new sources

Captures the 22 new files added in 72c122c and re-balances the existing
suppressions so the new model() / config() class-const fetches against
codeigniter4-standard rule sets are pinned to the new lines without
masking real issues elsewhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

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

53.33
/src/Models/DeviceSessionModel.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\I18n\Time;
17
use Daycry\Auth\Entities\DeviceSession;
18
use Daycry\Auth\Entities\User;
19
use Symfony\Component\Uid\Uuid;
20

21
class DeviceSessionModel extends BaseModel
22
{
23
    protected $primaryKey     = 'id';
24
    protected $returnType     = DeviceSession::class;
25
    protected $useSoftDeletes = false;
26
    protected $allowedFields  = [
27
        'uuid',
28
        'user_id',
29
        'session_id',
30
        'device_name',
31
        'ip_address',
32
        'user_agent',
33
        'last_active',
34
        'logged_out_at',
35
        'trusted_until',
36
    ];
37
    protected $useTimestamps = true;
38
    protected $createdField  = 'created_at';
39
    protected $updatedField  = 'updated_at';
40
    protected $beforeInsert  = ['generateUuid'];
41

42
    protected function initialize(): void
16✔
43
    {
44
        parent::initialize();
16✔
45

46
        $this->table = $this->tables['device_sessions'];
16✔
47
    }
48

49
    /**
50
     * Generates a UUID v7 for new device session records.
51
     *
52
     * Model event callback called by `beforeInsert`.
53
     *
54
     * @param array<string, mixed> $data
55
     *
56
     * @return array<string, mixed>
57
     */
58
    protected function generateUuid(array $data): array
15✔
59
    {
60
        if (empty($data['data']['uuid'])) {
15✔
61
            $data['data']['uuid'] = Uuid::v7()->toRfc4122();
15✔
62
        }
63

64
        return $data;
15✔
65
    }
66

67
    /**
68
     * Returns all active (not logged out) device sessions for the given user.
69
     *
70
     * @return list<DeviceSession>
71
     */
72
    public function getActiveForUser(User $user): array
11✔
73
    {
74
        return $this->where('user_id', $user->id)
11✔
75
            ->where('logged_out_at', null)
11✔
76
            ->orderBy('last_active', 'DESC')
11✔
77
            ->findAll();
11✔
78
    }
79

80
    /**
81
     * Returns all device sessions (active and terminated) for the given user.
82
     *
83
     * @return list<DeviceSession>
84
     */
85
    public function getAllForUser(User $user): array
×
86
    {
87
        return $this->where('user_id', $user->id)
×
88
            ->orderBy('last_active', 'DESC')
×
89
            ->findAll();
×
90
    }
91

92
    /**
93
     * Finds a device session by its PHP session ID.
94
     */
95
    public function findBySessionId(string $sessionId): ?DeviceSession
4✔
96
    {
97
        return $this->where('session_id', $sessionId)
4✔
98
            ->first();
4✔
99
    }
100

101
    /**
102
     * Creates a new device session record.
103
     */
104
    public function createSession(User $user, string $sessionId, string $ipAddress, ?string $userAgent = null): DeviceSession
15✔
105
    {
106
        $deviceName = DeviceSession::parseUserAgent($userAgent ?? '');
15✔
107

108
        $data = [
15✔
109
            'user_id'     => $user->id,
15✔
110
            'session_id'  => $sessionId,
15✔
111
            'device_name' => $deviceName,
15✔
112
            'ip_address'  => $ipAddress,
15✔
113
            'user_agent'  => $userAgent,
15✔
114
            'last_active' => Time::now()->format('Y-m-d H:i:s'),
15✔
115
        ];
15✔
116

117
        $id = $this->insert($data, true);
15✔
118

119
        /** @var DeviceSession|null $result */
120
        return $this->find($id);
15✔
121
    }
122

123
    /**
124
     * Updates the last_active timestamp for the given session.
125
     */
126
    public function touchSession(string $sessionId): void
1✔
127
    {
128
        $this->where('session_id', $sessionId)
1✔
129
            ->where('logged_out_at', null)
1✔
130
            ->set('last_active', Time::now()->format('Y-m-d H:i:s'))
1✔
131
            ->update();
1✔
132
    }
133

134
    /**
135
     * Marks the session as terminated by setting logged_out_at.
136
     */
137
    public function terminateSession(string $sessionId): void
4✔
138
    {
139
        $this->where('session_id', $sessionId)
4✔
140
            ->where('logged_out_at', null)
4✔
141
            ->set('logged_out_at', Time::now()->format('Y-m-d H:i:s'))
4✔
142
            ->update();
4✔
143
    }
144

145
    /**
146
     * Terminates all active sessions for a user, optionally keeping one session alive.
147
     *
148
     * @param string|null $exceptSessionId Session ID to keep active (e.g. current session)
149
     */
150
    public function terminateAllForUser(User $user, ?string $exceptSessionId = null): void
4✔
151
    {
152
        $builder = $this->where('user_id', $user->id)
4✔
153
            ->where('logged_out_at', null);
4✔
154

155
        if ($exceptSessionId !== null && $exceptSessionId !== '') {
4✔
156
            $builder = $builder->where('session_id !=', $exceptSessionId);
2✔
157
        }
158

159
        $builder->set('logged_out_at', Time::now()->format('Y-m-d H:i:s'))
4✔
160
            ->update();
4✔
161
    }
162

163
    /**
164
     * Removes old terminated sessions older than the given number of days.
165
     */
166
    public function purgeOldSessions(int $days = 30): void
1✔
167
    {
168
        $cutoff = Time::now()->subDays($days)->format('Y-m-d H:i:s');
1✔
169

170
        $this->where('logged_out_at <', $cutoff)
1✔
171
            ->delete();
1✔
172
    }
173

174
    /**
175
     * Marks the device session identified by $uuid as trusted for
176
     * $lifetimeSeconds. While `trusted_until` is in the future, 2FA
177
     * challenges can be skipped on this device.
178
     */
179
    public function markTrusted(string $uuid, int $lifetimeSeconds): void
×
180
    {
181
        if ($uuid === '' || $lifetimeSeconds <= 0) {
×
182
            return;
×
183
        }
184

185
        $until = Time::now()->addSeconds($lifetimeSeconds)->format('Y-m-d H:i:s');
×
186

187
        $this->where('uuid', $uuid)
×
188
            ->set('trusted_until', $until)
×
189
            ->update();
×
190
    }
191

192
    /**
193
     * Returns the device session identified by $uuid when it is currently
194
     * trusted (logged-in, not revoked, and `trusted_until` is in the future).
195
     */
196
    public function findTrustedByUuid(string $uuid): ?DeviceSession
×
197
    {
198
        if ($uuid === '') {
×
199
            return null;
×
200
        }
201

202
        $now = Time::now()->format('Y-m-d H:i:s');
×
203

204
        $row = $this->where('uuid', $uuid)
×
205
            ->where('logged_out_at', null)
×
206
            ->where('trusted_until >=', $now)
×
207
            ->first();
×
208

209
        return $row instanceof DeviceSession ? $row : null;
×
210
    }
211

212
    /**
213
     * Clears the trust flag from the given device session (e.g. when the
214
     * user explicitly revokes it).
215
     */
216
    public function revokeTrust(string $uuid): void
×
217
    {
218
        if ($uuid === '') {
×
219
            return;
×
220
        }
221

222
        $this->where('uuid', $uuid)
×
223
            ->set('trusted_until', null)
×
224
            ->update();
×
225
    }
226

227
    /**
228
     * Enforces a per-user limit on concurrent active sessions.
229
     *
230
     * If the user already has `>= $limit` active sessions, terminates the
231
     * oldest ones (by `last_active` ascending) until exactly `$limit - 1`
232
     * remain — leaving room for the new session about to be created.
233
     *
234
     * @return int Number of sessions terminated.
235
     */
236
    public function enforceConcurrentSessionLimit(User $user, int $limit): int
×
237
    {
238
        if ($limit <= 0) {
×
239
            return 0;
×
240
        }
241

242
        $active = $this->where('user_id', $user->id)
×
243
            ->where('logged_out_at', null)
×
244
            ->orderBy('last_active', 'ASC')
×
245
            ->findAll();
×
246

247
        $excess = count($active) - ($limit - 1);
×
248

249
        if ($excess <= 0) {
×
250
            return 0;
×
251
        }
252

253
        $terminated = 0;
×
254

255
        foreach (array_slice($active, 0, $excess) as $session) {
×
256
            if (! empty($session->session_id)) {
×
257
                $this->terminateSession((string) $session->session_id);
×
258
                $terminated++;
×
259
            }
260
        }
261

262
        return $terminated;
×
263
    }
264
}
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