add CRM module start

This commit is contained in:
root
2026-01-15 07:09:36 +03:00
parent b14f293a45
commit b810a17649
40 changed files with 3807 additions and 14 deletions
+197
View File
@@ -0,0 +1,197 @@
<?php
namespace App\Libraries;
/**
* RateLimitIdentifier — генератор идентификаторов для rate limiting
*
* Использует комбинацию:
* - Cookie token (если есть) — персональный идентификатор браузера
* - IP адрес — базовая идентификация
* - User Agent hash — дополнительная уникальность
*/
class RateLimitIdentifier
{
private const COOKIE_NAME = 'rl_token';
private const COOKIE_TTL = 365 * 24 * 3600; // 1 год
/**
* Получает уникальный идентификатор для rate limiting
*
* @param string $action Действие (login, register, reset)
* @return string Хеш-идентификатор
*/
public function getIdentifier(string $action): string
{
$parts = [];
// Cookie token — если есть, добавляем
$cookieToken = $_COOKIE[self::COOKIE_NAME] ?? null;
if ($cookieToken && $this->isValidToken($cookieToken)) {
$parts['c'] = $cookieToken;
}
// IP адрес — всегда
$parts['i'] = $this->getClientIp();
// User Agent hash — всегда (для дополнительной уникальности)
$parts['ua'] = $this->getUserAgentHash();
// Если куки нет — добавляем признак "без куки" для отладки
if (empty($cookieToken)) {
$parts['nc'] = '1';
}
// Комбинация всех частей → хеш
return md5('rl:' . $action . ':' . implode('|', $parts));
}
/**
* Генерирует и устанавливает токен, если его нет
*
* @return string|null Токен или null если уже есть
*/
public function ensureToken(): ?string
{
if (empty($_COOKIE[self::COOKIE_NAME])) {
$token = $this->generateToken();
// Устанавливаем куку на год
setcookie(
self::COOKIE_NAME,
$token,
[
'expires' => time() + self::COOKIE_TTL,
'path' => '/',
'secure' => true,
'samesite' => 'Lax',
'httponly' => true,
]
);
return $token;
}
return null;
}
/**
* Проверяет, установлен ли токен
*
* @return bool
*/
public function hasToken(): bool
{
return !empty($_COOKIE[self::COOKIE_NAME]);
}
/**
* Получает JS код для установки токена при первом визите
*
* @return string JavaScript код
*/
public function getJsScript(): string
{
return <<<JS
(function() {
const cookieName = 'rl_token';
// Проверяем, есть ли кука
const hasCookie = document.cookie.split('; ').find(function(row) {
return row.indexOf(cookieName + '=') === 0;
});
if (!hasCookie) {
// Генерируем UUID v4 на клиенте
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
const token = generateUUID();
const date = new Date();
date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000));
const cookieString = cookieName + '=' + token +
'; expires=' + date.toUTCString() +
'; path=/' +
'; SameSite=Lax' +
(location.protocol === 'https:' ? '; Secure' : '') +
'; HttpOnly';
document.cookie = cookieString;
}
})();
JS;
}
/**
* Генерирует уникальный токен
*
* @return string 32 символа (16 байт в hex)
*/
private function generateToken(): string
{
return bin2hex(random_bytes(16));
}
/**
* Проверяет валидность токена
*
* @param string $token
* @return bool
*/
private function isValidToken(string $token): bool
{
// Токен должен быть 32 hex символа (16 байт)
return preg_match('/^[a-f0-9]{32}$/', $token) === 1;
}
/**
* Получает IP адрес клиента
*
* @return string
*/
private function getClientIp(): string
{
$ipKeys = [
'HTTP_CF_CONNECTING_IP', // Cloudflare
'HTTP_X_REAL_IP', // Nginx proxy
'HTTP_X_FORWARDED_FOR', // Load balancer
'REMOTE_ADDR',
];
foreach ($ipKeys as $key) {
if (!empty($_SERVER[$key])) {
$ips = explode(',', $_SERVER[$key]);
return trim($ips[0]);
}
}
return '0.0.0.0';
}
/**
* Получает хеш User Agent
*
* Используем хеш вместо полного UA для:
* - Короче значение
* - Не храним чувствительные данные
*
* @return string
*/
private function getUserAgentHash(): string
{
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
// Обрезаем слишком длинные UA
if (strlen($ua) > 500) {
$ua = substr($ua, 0, 500);
}
return md5($ua);
}
}