Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.74% covered (success)
94.74%
72 / 76
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AuthLogService
94.74% covered (success)
94.74%
72 / 76
33.33% covered (danger)
33.33%
2 / 6
31.14
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 logSuccess
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
 logFailure
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
 getLogs
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
5
 getCurrentMicrosecondDatetime
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 hydrateLog
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
17
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Auth\Service;
6
7use App\Domain\Auth\ValueObject\AuthCause;
8use App\Domain\Auth\ValueObject\AuthStatus;
9use DateTimeImmutable;
10use DateTimeZone;
11use Yiisoft\Db\Connection\ConnectionInterface;
12use Yiisoft\Db\Query\Query;
13
14 class AuthLogService
15{
16    private const DEFAULT_IP = '127.0.0.1';
17    private const TABLE_NAME = '{{%logs_auth}}';
18
19    public function __construct(private ConnectionInterface $db) {}
20
21    public function logSuccess(
22        int $userId,
23        string $ipAddress
24    ): void {
25        $binaryIp = inet_pton($ipAddress);
26        if ($binaryIp === false) {
27            $binaryIp = inet_pton(self::DEFAULT_IP) ?: '';
28        }
29
30        $this->db->createCommand()->insert(self::TABLE_NAME, [
31            'status' => AuthStatus::SUCCESS->value,
32            'user_id' => $userId,
33            'ip_address' => $binaryIp,
34            'cause' => null,
35            'created_at' => $this->getCurrentMicrosecondDatetime(),
36        ])->execute();
37    }
38
39    public function logFailure(
40        string $ipAddress,
41        ?int $userId = null,
42        ?AuthCause $cause = AuthCause::INVALID_PASSWORD
43    ): void {
44        $binaryIp = inet_pton($ipAddress);
45        if ($binaryIp === false) {
46            $binaryIp = inet_pton(self::DEFAULT_IP) ?: '';
47        }
48
49        $this->db->createCommand()->insert(self::TABLE_NAME, [
50            'status' => AuthStatus::FAILURE->value,
51            'user_id' => $userId,
52            'ip_address' => $binaryIp,
53            'cause' => $cause?->value,
54            'created_at' => $this->getCurrentMicrosecondDatetime(),
55        ])->execute();
56    }
57
58    /**
59     * @return array{
60     *     target: string,
61     *     items: array<int, array{
62     *         id: int,
63     *         status: int,
64     *         user_id: int|null,
65     *         ip_address: string,
66     *         cause: int|null,
67     *         cause_description: string,
68     *         created_at: string
69     *     }>,
70     *     page: int,
71     *     limit: int,
72     *     total_count: int,
73     *     total_pages: int
74     * }
75     */
76    public function getLogs(string $target = 'server', int $page = 1, int $limit = 20): array
77    {
78        $target = in_array($target, ['server', 'client'], true) ? $target : 'server';
79        $limit = max(1, min(100, $limit));
80        $page = max(1, $page);
81        $offset = ($page - 1) * $limit;
82
83        $driverName = $this->db->getDriverName();
84        $tableName = self::TABLE_NAME;
85
86        if ($target === 'client') {
87            $tableName = $driverName === 'mysql' ? 'ammonly_client.c01_logs_auth' : 'c01_logs_auth';
88        }
89
90        $countQuery = (new Query($this->db))->from($tableName);
91        $totalCount = (int) $countQuery->count();
92
93        $rows = (new Query($this->db))
94            ->from($tableName)
95            ->orderBy(['created_at' => SORT_DESC, 'id' => SORT_DESC])
96            ->offset($offset)
97            ->limit($limit)
98            ->all();
99
100        $items = [];
101        foreach ($rows as $row) {
102            $items[] = $this->hydrateLog((array) $row);
103        }
104
105        $totalPages = (int) ceil($totalCount / $limit);
106
107        return [
108            'target' => $target,
109            'items' => $items,
110            'page' => $page,
111            'limit' => $limit,
112            'total_count' => $totalCount,
113            'total_pages' => max(1, $totalPages),
114        ];
115    }
116
117    private function getCurrentMicrosecondDatetime(): string
118    {
119        $dt = DateTimeImmutable::createFromFormat('U.u', sprintf('%.6f', microtime(true)));
120        if ($dt === false) {
121            return gmdate('Y-m-d H:i:s.000000');
122        }
123
124        return $dt->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s.u');
125    }
126
127    /**
128     * @param array<string, mixed> $row
129     * @return array{id: int, status: int, user_id: int|null, ip_address: string, cause: int|null, cause_description: string, created_at: string}
130     */
131    private function hydrateLog(array $row): array
132    {
133        $binaryIp = $row['ip_address'] ?? '';
134        $ipFormatted = self::DEFAULT_IP;
135        if (is_string($binaryIp) && $binaryIp !== '') {
136            $converted = inet_ntop($binaryIp);
137            if ($converted !== false) {
138                $ipFormatted = $converted;
139            }
140        }
141
142        $causeValue = isset($row['cause']) && is_numeric($row['cause']) ? (int) $row['cause'] : null;
143        $causeEnum = $causeValue !== null ? AuthCause::tryFrom($causeValue) : null;
144
145        return [
146            'id' => is_numeric($row['id'] ?? null) ? (int) $row['id'] : 0,
147            'status' => is_numeric($row['status'] ?? null) ? (int) $row['status'] : 0,
148            'user_id' => isset($row['user_id']) && is_numeric($row['user_id']) ? (int) $row['user_id'] : null,
149            'ip_address' => $ipFormatted,
150            'cause' => $causeValue,
151            'cause_description' => match ($causeEnum) {
152                AuthCause::BLOCKED => '0 - Zablokowany',
153                AuthCause::LOGIN_NOT_FOUND => '1 - NieistniejÄ…cy login',
154                AuthCause::USER_INACTIVE => '2 - Nieaktywny',
155                AuthCause::INVALID_PASSWORD => '3 - Niepoprawne hasÅ‚o',
156                default => '-',
157            },
158            'created_at' => is_string($row['created_at'] ?? null) ? $row['created_at'] : '',
159        ];
160    }
161}