• 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

92.31
/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
    ];
36
    protected $useTimestamps = true;
37
    protected $createdField  = 'created_at';
38
    protected $updatedField  = 'updated_at';
39
    protected $beforeInsert  = ['generateUuid'];
40

41
    protected function initialize(): void
13✔
42
    {
43
        parent::initialize();
13✔
44

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

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

63
        return $data;
13✔
64
    }
65

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

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

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

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

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

116
        $id = $this->insert($data, true);
13✔
117

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

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

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

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

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

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

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

169
        $this->where('logged_out_at <', $cutoff)
1✔
170
            ->delete();
1✔
171
    }
172
}
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