Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
34.38% covered (danger)
34.38%
22 / 64
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AuthLogService
34.38% covered (danger)
34.38%
22 / 64
16.67% covered (danger)
16.67%
1 / 6
284.36
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
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 getCurrentMicrosecondDatetime
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 hydrateLog
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
306
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{items: array<int, array{id: int, status: int, user_id: int|null, ip_address: string, cause: int|null, cause_description: string, created_at: string}>, total: int}
60     */
61    public function getLogs(int $offset, int $limit, string $target = 'default'): array
62    {
63        $driverName = $this->db->getDriverName();
64        $tableName = self::TABLE_NAME;
65
66        if ($target === 'client') {
67            $tableName = $driverName === 'mysql' ? 'ammonly_client.c01_logs_auth' : 'c01_logs_auth';
68        }
69
70        $countQuery = (new Query($this->db))->from($tableName);
71        $totalCount = (int) $countQuery->count();
72
73        $rows = (new Query($this->db))
74            ->from($tableName)
75            ->orderBy(['created_at' => SORT_DESC, 'id' => SORT_DESC])
76            ->offset($offset)
77            ->limit($limit)
78            ->all();
79
80        $items = [];
81        foreach ($rows as $row) {
82            $items[] = $this->hydrateLog((array) $row);
83        }
84
85        return ['items' => $items, 'total' => $totalCount];
86    }
87
88    private function getCurrentMicrosecondDatetime(): string
89    {
90        $dt = DateTimeImmutable::createFromFormat('U.u', sprintf('%.6f', microtime(true)));
91        if ($dt === false) {
92            return gmdate('Y-m-d H:i:s.000000');
93        }
94
95        return $dt->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s.u');
96    }
97
98    /**
99     * @param array<string, mixed> $row
100     * @return array{id: int, status: int, user_id: int|null, ip_address: string, cause: int|null, cause_description: string, created_at: string}
101     */
102    private function hydrateLog(array $row): array
103    {
104        $binaryIp = $row['ip_address'] ?? '';
105        $ipFormatted = self::DEFAULT_IP;
106        if (is_string($binaryIp) && $binaryIp !== '') {
107            $converted = inet_ntop($binaryIp);
108            if ($converted !== false) {
109                $ipFormatted = $converted;
110            }
111        }
112
113        $causeValue = isset($row['cause']) && is_numeric($row['cause']) ? (int) $row['cause'] : null;
114        $causeEnum = $causeValue !== null ? AuthCause::tryFrom($causeValue) : null;
115
116        return [
117            'id' => is_numeric($row['id'] ?? null) ? (int) $row['id'] : 0,
118            'status' => is_numeric($row['status'] ?? null) ? (int) $row['status'] : 0,
119            'user_id' => isset($row['user_id']) && is_numeric($row['user_id']) ? (int) $row['user_id'] : null,
120            'ip_address' => $ipFormatted,
121            'cause' => $causeValue,
122            'cause_description' => match ($causeEnum) {
123                AuthCause::BLOCKED => '0 - Zablokowany',
124                AuthCause::LOGIN_NOT_FOUND => '1 - NieistniejÄ…cy login',
125                AuthCause::USER_INACTIVE => '2 - Nieaktywny',
126                AuthCause::INVALID_PASSWORD => '3 - Niepoprawne hasÅ‚o',
127                default => '-',
128            },
129            'created_at' => is_string($row['created_at'] ?? null) ? $row['created_at'] : '',
130        ];
131    }
132}