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

daycry / auth / 25518434194

07 May 2026 07:49PM UTC coverage: 58.608% (-6.4%) from 64.989%
25518434194

push

github

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

Implement security enhancements and new account features

277 of 1030 new or added lines in 55 files covered. (26.89%)

11 existing lines in 6 files now uncovered.

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

78.57
/src/Services/AuditLogger.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\Services;
15

16
use CodeIgniter\HTTP\IncomingRequest;
17
use CodeIgniter\I18n\Time;
18
use Daycry\Auth\Models\AuditLogModel;
19
use Throwable;
20

21
/**
22
 * Records granular account/security events to the auth_audit_logs table.
23
 *
24
 * Hook points fire calls to {@see record()} from sensitive flows:
25
 *   - 2FA enable/disable
26
 *   - password change, email change, role change
27
 *   - lockout triggered
28
 *   - token revoke (access / JWT refresh)
29
 *   - trusted device add/remove
30
 *
31
 * Failures (DB unavailable, schema missing) are caught and logged at
32
 * `warning` level — audit failure must never break the user-facing flow.
33
 */
34
class AuditLogger
35
{
36
    /**
37
     * Canonical event type identifiers. Using constants prevents typos in
38
     * caller code and keeps the vocabulary discoverable.
39
     */
40
    public const EVENT_TOTP_ENABLED = 'totp.enabled';
41

42
    public const EVENT_TOTP_DISABLED          = 'totp.disabled';
43
    public const EVENT_TOTP_ADMIN_RESET       = 'totp.admin_reset';
44
    public const EVENT_PASSWORD_CHANGED       = 'password.changed';
45
    public const EVENT_PASSWORD_RESET         = 'password.reset';
46
    public const EVENT_EMAIL_CHANGE_REQUEST   = 'email.change_request';
47
    public const EVENT_EMAIL_CHANGED          = 'email.changed';
48
    public const EVENT_USER_LOCKED            = 'user.locked';
49
    public const EVENT_USER_UNLOCKED          = 'user.unlocked';
50
    public const EVENT_GROUP_ASSIGNED         = 'group.assigned';
51
    public const EVENT_GROUP_REVOKED          = 'group.revoked';
52
    public const EVENT_PERMISSION_GRANTED     = 'permission.granted';
53
    public const EVENT_PERMISSION_REVOKED     = 'permission.revoked';
54
    public const EVENT_TOKEN_REVOKED          = 'token.revoked';
55
    public const EVENT_REFRESH_TOKEN_REVOKED  = 'token.refresh_revoked';
56
    public const EVENT_TRUSTED_DEVICE_ADDED   = 'device.trusted_added';
57
    public const EVENT_TRUSTED_DEVICE_REMOVED = 'device.trusted_removed';
58
    public const EVENT_OAUTH_LINKED           = 'oauth.linked';
59
    public const EVENT_OAUTH_UNLINKED         = 'oauth.unlinked';
60
    public const EVENT_SUSPICIOUS_LOGIN       = 'login.suspicious';
61
    public const EVENT_USER_ANONYMIZED        = 'user.anonymized';
62

63
    /**
64
     * Records a single audit event.
65
     *
66
     * @param string               $eventType One of the EVENT_* constants (or a custom snake_case identifier).
67
     * @param int|null             $userId    Affected user id.
68
     * @param array<string, mixed> $metadata  Extra context (will be JSON-encoded).
69
     * @param int|null             $actorId   User id that triggered the event when different from $userId
70
     *                                        (admin actions). Defaults to $userId.
71
     */
72
    public function record(string $eventType, ?int $userId, array $metadata = [], ?int $actorId = null): void
50✔
73
    {
74
        try {
75
            /** @var AuditLogModel $model */
76
            $model = model(AuditLogModel::class);
50✔
77

78
            [$ip, $ua] = $this->resolveRequestContext();
50✔
79

80
            $model->insert([
50✔
81
                'user_id'    => $userId,
50✔
82
                'actor_id'   => $actorId ?? $userId,
50✔
83
                'event_type' => $eventType,
50✔
84
                'ip_address' => $ip,
50✔
85
                'user_agent' => $ua,
50✔
86
                'metadata'   => $metadata === [] ? null : json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
50✔
87
                'created_at' => Time::now()->toDateTimeString(),
50✔
88
            ]);
50✔
NEW
89
        } catch (Throwable $e) {
×
NEW
90
            log_message('warning', 'AuditLogger::record failed for {event}: {message}', [
×
NEW
91
                'event'   => $eventType,
×
NEW
92
                'message' => $e->getMessage(),
×
NEW
93
            ]);
×
94
        }
95
    }
96

97
    /**
98
     * @return array{0: string|null, 1: string|null}
99
     */
100
    private function resolveRequestContext(): array
50✔
101
    {
102
        $ip = null;
50✔
103
        $ua = null;
50✔
104

105
        try {
106
            $request = service('request');
50✔
107

108
            if ($request instanceof IncomingRequest) {
50✔
109
                $ip = $request->getIPAddress();
50✔
110
                $ua = (string) $request->getUserAgent();
50✔
111

112
                if ($ua === '') {
50✔
113
                    $ua = null;
50✔
114
                }
115
            }
NEW
116
        } catch (Throwable) {
×
117
            // CLI context or service container not ready — fine to skip.
118
        }
119

120
        return [$ip, $ua];
50✔
121
    }
122
}
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