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

daycry / auth / 25552752016

08 May 2026 11:20AM UTC coverage: 71.592% (+13.0%) from 58.608%
25552752016

push

github

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

Add Laravel-parity authentication features: Gates, Password Confirmation, Basic Auth

198 of 252 new or added lines in 18 files covered. (78.57%)

1 existing line in 1 file now uncovered.

4453 of 6220 relevant lines covered (71.59%)

62.44 hits per line

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

88.24
/src/Commands/AuditCommand.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\Commands;
15

16
use CodeIgniter\CLI\CLI;
17
use CodeIgniter\I18n\Time;
18
use Daycry\Auth\Models\AuditLogModel;
19
use Daycry\Auth\Models\UserModel;
20
use InvalidArgumentException;
21
use Throwable;
22

23
/**
24
 * Reads recent entries from the audit log table.
25
 *
26
 * Usage:
27
 *   php spark auth:audit
28
 *   php spark auth:audit --since=24h
29
 *   php spark auth:audit --user=user@example.com
30
 *   php spark auth:audit --type=totp.enabled
31
 *   php spark auth:audit --limit=50
32
 */
33
class AuditCommand extends BaseCommand
34
{
35
    protected $name        = 'auth:audit';
36
    protected $description = 'Show recent entries from the audit log.';
37
    protected $usage       = 'auth:audit [--since=24h] [--user=<email>] [--type=<event_type>] [--limit=<n>]';
38

39
    /**
40
     * @var array<string, string>
41
     */
42
    protected $options = [
43
        '--since' => 'Time window, e.g. 24h, 7d, 30d (default: 7d).',
44
        '--user'  => 'Filter by user email.',
45
        '--type'  => 'Filter by event_type (e.g. totp.enabled).',
46
        '--limit' => 'Maximum number of rows to display (default: 100, max: 500).',
47
    ];
48

49
    public function run(array $params): int
7✔
50
    {
51
        $since = (string) ($params['since'] ?? '7d');
7✔
52
        $email = (string) ($params['user'] ?? '');
7✔
53
        $type  = (string) ($params['type'] ?? '');
7✔
54
        $limit = max(1, min(500, (int) ($params['limit'] ?? 100)));
7✔
55

56
        try {
57
            $cutoff = $this->parseSince($since);
7✔
58
        } catch (Throwable $e) {
1✔
59
            $this->error('Invalid --since value: ' . $e->getMessage());
1✔
60

61
            return 1;
1✔
62
        }
63

64
        try {
65
            /** @var AuditLogModel $auditModel */
66
            $auditModel = model(AuditLogModel::class);
6✔
67

68
            $builder = $auditModel
6✔
69
                ->where('created_at >=', $cutoff->toDateTimeString())
6✔
70
                ->orderBy('id', 'DESC')
6✔
71
                ->limit($limit);
6✔
72

73
            if ($type !== '') {
6✔
74
                $builder = $builder->where('event_type', $type);
1✔
75
            }
76

77
            if ($email !== '') {
6✔
78
                /** @var UserModel $userModel */
79
                $userModel = model(UserModel::class);
2✔
80
                $user      = $userModel->findByCredentials(['email' => $email]);
2✔
81

82
                if ($user === null) {
2✔
83
                    $this->error('User not found: ' . $email);
1✔
84

85
                    return 1;
1✔
86
                }
87

88
                $builder = $builder->where('user_id', $user->id);
1✔
89
            }
90

91
            $rows = $builder->find();
5✔
92
        } catch (Throwable $e) {
×
NEW
93
            $this->error('Audit query failed: ' . $e->getMessage());
×
94

95
            return 1;
×
96
        }
97

98
        if ($rows === []) {
5✔
99
            $this->write('No audit entries match the filters.', 'yellow');
1✔
100

101
            return 0;
1✔
102
        }
103

104
        $head = ['ID', 'When', 'Event', 'User', 'IP', 'Metadata'];
4✔
105
        $body = [];
4✔
106

107
        foreach ($rows as $row) {
4✔
108
            $userId  = is_object($row) ? ($row->user_id ?? null) : ($row['user_id'] ?? null);
4✔
109
            $event   = is_object($row) ? ($row->event_type ?? '') : ($row['event_type'] ?? '');
4✔
110
            $created = is_object($row) ? ($row->created_at ?? '') : ($row['created_at'] ?? '');
4✔
111
            $ip      = is_object($row) ? ($row->ip_address ?? '') : ($row['ip_address'] ?? '');
4✔
112
            $rawMeta = is_object($row) ? ($row->metadata ?? null) : ($row['metadata'] ?? null);
4✔
113
            $rowId   = is_object($row) ? ($row->id ?? '') : ($row['id'] ?? '');
4✔
114

115
            $body[] = [
4✔
116
                (string) $rowId,
4✔
117
                (string) $created,
4✔
118
                (string) $event,
4✔
119
                $userId === null ? '' : (string) $userId,
4✔
120
                (string) $ip,
4✔
121
                is_string($rawMeta) ? $this->shortMeta($rawMeta) : '',
4✔
122
            ];
4✔
123
        }
124

125
        CLI::table($body, $head);
4✔
126

127
        return 0;
4✔
128
    }
129

130
    /**
131
     * Parses values like `24h`, `7d`, `30d`, `1w` into an absolute Time.
132
     */
133
    private function parseSince(string $expr): Time
7✔
134
    {
135
        $expr = strtolower(trim($expr));
7✔
136

137
        if ($expr === '' || ! preg_match('/^(\d+)([smhdw])$/', $expr, $m)) {
7✔
138
            throw new InvalidArgumentException("expected NNs|m|h|d|w, got '{$expr}'");
1✔
139
        }
140

141
        $n   = (int) $m[1];
6✔
142
        $now = Time::now();
6✔
143

144
        return match ($m[2]) {
6✔
145
            's'     => $now->subSeconds($n),
1✔
146
            'm'     => $now->subMinutes($n),
1✔
147
            'h'     => $now->subHours($n),
1✔
148
            'd'     => $now->subDays($n),
6✔
149
            'w'     => $now->subDays($n * 7),
1✔
150
            default => $now->subDays(7),
6✔
151
        };
6✔
152
    }
153

154
    /**
155
     * Truncates a metadata JSON string to a single line for tabular display.
156
     */
157
    private function shortMeta(string $json): string
×
158
    {
159
        $compact = preg_replace('/\s+/', ' ', $json) ?? $json;
×
160

161
        return mb_strlen($compact) > 60
×
162
            ? mb_substr($compact, 0, 57) . '...'
×
163
            : $compact;
×
164
    }
165
}
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