From c520b87af5b5cb9ba4bb96ec6417a0d947db5666 Mon Sep 17 00:00:00 2001 From: velz Date: Mon, 23 Feb 2026 15:46:05 +0530 Subject: [PATCH] FEAT_API_RATE_LIMIT_COND --- app/Config/Routes.php | 4 +- app/Config/Services.php | 10 + .../RestAuthenticationController.php | 6 + app/Filters/AuthApiRateLimitFilter.php | 61 ++- app/Filters/JwtApiRateLimitFilter.php | 8 +- app/Helpers/HttpRequestHelper.php | 1 + app/Helpers/utility_helper.php | 103 ++++- app/Libraries/RateLimiterService.php | 385 ++++++++++++++++++ 8 files changed, 528 insertions(+), 50 deletions(-) create mode 100644 app/Libraries/RateLimiterService.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index a0eb3e0..cb4c782 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -454,7 +454,7 @@ $routes->group("/api", ["filter" => [ 'ratelimit' , 'authJWT' ] ], function ($r // $routes->post("employeeRest/createOrUpdateEmployeePolicySiAmount", "EmployeeRestController::createOrUpdateEmployeePolicySiAmount"); // $routes->post("employeeRest/calculatePremium", "EmployeeRestController::calculatePremium"); // $routes->post("updateMpin", "RestAuthenticationController::updateMpin"); -$routes->group("employeeRest", ['filter' => [ 'GlobalPostFileUploadGuard', 'ratelimit' , 'appSignature' , 'authJWT' ] ], function ($routes) { +$routes->group("employeeRest", ['filter' => [ 'GlobalPostFileUploadGuard', 'appSignature' , 'authJWT','JwtApiRateLimitFilter' ] ], function ($routes) { $routes->post('logout', 'RestAuthenticationController::logout'); @@ -507,7 +507,7 @@ $routes->group("employeeRest", ['filter' => [ 'GlobalPostFileUploadGuard', 'rate }); -$routes->group("employeeRest", ['filter' => ['ratelimit' , 'appSignature'] ], function ($routes) { +$routes->group("employeeRest", ['filter' => ['appSignature','AuthApiRateLimitFilter'] ], function ($routes) { //Employee login api's $routes->post("verifyEmployeeNumber", "RestAuthenticationController::verifyEmployeeWithMobileNumber"); diff --git a/app/Config/Services.php b/app/Config/Services.php index fab85a5..d37961a 100755 --- a/app/Config/Services.php +++ b/app/Config/Services.php @@ -8,6 +8,7 @@ use App\Libraries\MyLogger; use App\Libraries\GmailAPI; use App\Libraries\MyGoogleDrive; use App\Libraries\DataServiceSqlite; +use App\Libraries\RateLimiterService; use App\Controllers\Home; /** @@ -80,5 +81,14 @@ class Services extends BaseService return new MyGoogleDrive(); } + + public static function limiter($getShared = true) + { + if ($getShared) { + return static::getSharedInstance('limiter'); + } + + return new RateLimiterService(); + } } diff --git a/app/Controllers/RestAuthenticationController.php b/app/Controllers/RestAuthenticationController.php index b15d191..e29c0a7 100755 --- a/app/Controllers/RestAuthenticationController.php +++ b/app/Controllers/RestAuthenticationController.php @@ -142,6 +142,7 @@ class RestAuthenticationController extends AdminController log_message('error', ' '); log_message('error', '************************ PRE END ********************************'); $result = ['user_verification' => false , 'message' => "User not found"]; + recordRateLimitFailure(); return $this->respond(['status' => 'failed','code' => 404,'data' => $result],200); } @@ -320,6 +321,7 @@ class RestAuthenticationController extends AdminController } $result = ['user_verification' => false , 'message' => "User not found"]; + recordRateLimitFailure(); return $this->respond(['status' => 'failed','code' => 404,'data' => $result],200); } @@ -445,10 +447,12 @@ class RestAuthenticationController extends AdminController $otp = isset($this->request->getJSON()->otp) ? $this->request->getJSON()->otp : null; if(empty($otp)){ + recordRateLimitFailure(); return $this->respond(['status' => 'OTP is required','code' => 400,'message' => 'OTP is required'], 200); } if (empty($mobile_number) && empty($email_id)) { + recordRateLimitFailure(); return $this->respond(['status' => 'failed','code' => 400,'message' => 'Mobile number or Email ID is required'], 200); } @@ -472,9 +476,11 @@ class RestAuthenticationController extends AdminController // Call the third-party API function $apiResponse = $this->callThirdPartyAPI($retailApiParams, 'getVerifiedRetailUserData'); // print_r($apiResponse); die; + recordRateLimitFailure(); return $this->respond(['status' => 'failed', 'code' => 200, 'data' => [], 'post_enrollment' => json_decode($apiResponse, true)], 200); } + recordRateLimitFailure(); return $this->respond(['status' => 'Invalid OTP','code' => 404,'data' => "", 'message' => "Invalid OTP"],200); } diff --git a/app/Filters/AuthApiRateLimitFilter.php b/app/Filters/AuthApiRateLimitFilter.php index 3dc0db2..29ce42f 100644 --- a/app/Filters/AuthApiRateLimitFilter.php +++ b/app/Filters/AuthApiRateLimitFilter.php @@ -38,8 +38,9 @@ class AuthApiRateLimitFilter implements FilterInterface public function before(RequestInterface $request, $arguments = null) { + // $this->limiter->unblockUser('9698262411');die; $fingerprint = generateFingerprint(); - + // echo $fingerprint;die; // 1. IP-level check $ipResult = $this->limiter->checkIp($fingerprint, 'authApi'); if ($ipResult) { @@ -47,7 +48,8 @@ class AuthApiRateLimitFilter implements FilterInterface } // 2. User-level block check (identity may not be present yet on first hit) - $identity = $this->resolveIdentity($request); + $identity = resolveIdentity($request); + // echo $identity;die; if ($identity) { $userResult = $this->limiter->checkUser($identity); if ($userResult) { @@ -70,15 +72,33 @@ class AuthApiRateLimitFilter implements FilterInterface public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { + + // Routes where rate-limit failure should be recorded + // $allowedRoutes = [ + // 'employeeRest/verifyEmployeeNumber', + // 'employeeRest/getVerifiedUserData', + // 'employeeRest/verifyEmployeeEmailId', + // 'employeeRest/verifyHrWithMobileNumber', + // 'employeeRest/verifyHrWithEmail', + // 'employeeRest/verifyHrWithEmail', + // 'employeeRest/getVerifiedHrData', + // ]; + + // $currentPath = service('request')->getPath(); + // if (!in_array($currentPath, $allowedRoutes)) { + // return; // Don't record failures for unrelated routes + // } + // Only act on failed responses (4xx from auth failures) $statusCode = $response->getStatusCode(); + // print_r($response);die(); if ($statusCode < 400 || $statusCode === 429 || $statusCode === 403 || $statusCode === 451) { return; // 2xx/3xx = success; 429/403/451 already handled } - $fingerprint = $request->getGlobal('rateLimitFingerprint') ?? generateFingerprint(); - $identity = $request->getGlobal('rateLimitIdentity') - ?? $this->resolveIdentity($request); + $fingerprint = $request->getVar('rateLimitFingerprint') ?? generateFingerprint(); + $identity = $request->getVar('rateLimitIdentity') + ?? resolveIdentity($request); // Record failure at IP level $this->limiter->recordIpFailure($fingerprint); @@ -89,37 +109,6 @@ class AuthApiRateLimitFilter implements FilterInterface } } - // ------------------------------------------------------------------------- - // HELPERS - // ------------------------------------------------------------------------- - - /** - * Extract identity from POST body or GET params. - * Looks for 'email' or 'mobile_number'. - */ - protected function resolveIdentity(RequestInterface $request): ?string - { - // Try POST body first - $email = $request->getPost('email'); - $mobile = $request->getPost('mobile_number'); - - // Fallback to GET params - if (! $email && ! $mobile) { - $email = $request->getGet('email'); - $mobile = $request->getGet('mobile_number'); - } - - if ($email) { - return strtolower(trim($email)); - } - - if ($mobile) { - return trim($mobile); - } - - return null; - } - /** * Build and return a JSON response for blocked/throttled requests. */ diff --git a/app/Filters/JwtApiRateLimitFilter.php b/app/Filters/JwtApiRateLimitFilter.php index 2c887b6..1644aea 100644 --- a/app/Filters/JwtApiRateLimitFilter.php +++ b/app/Filters/JwtApiRateLimitFilter.php @@ -40,7 +40,7 @@ class JwtApiRateLimitFilter implements FilterInterface public function before(RequestInterface $request, $arguments = null) { $fingerprint = generateFingerprint(); - + // echo $fingerprint;die; // 1. IP-level throttle + block check $ipResult = $this->limiter->checkIp($fingerprint, 'jwtApi'); if ($ipResult) { @@ -50,7 +50,8 @@ class JwtApiRateLimitFilter implements FilterInterface // 2. Resolve user identity from JWT // Uses your existing JWT helper functions. // If neither returns a value, fall back to IP-only limiting. - $identity = $this->resolveIdentityFromJwt(); + // $identity = $this->resolveIdentityFromJwt(); + $identity = ''; if ($identity) { // User-level throttle (request count based for JWT routes) @@ -84,7 +85,8 @@ class JwtApiRateLimitFilter implements FilterInterface } $fingerprint = $request->getGlobal('rateLimitFingerprint') ?? generateFingerprint(); - $identity = $request->getGlobal('rateLimitIdentity') ?? $this->resolveIdentityFromJwt(); + // $identity = $request->getGlobal('rateLimitIdentity') ?? $this->resolveIdentityFromJwt(); + $identity = $request->getGlobal('rateLimitIdentity') ?? ''; $this->limiter->recordIpFailure($fingerprint); diff --git a/app/Helpers/HttpRequestHelper.php b/app/Helpers/HttpRequestHelper.php index 3e072e8..53268b3 100755 --- a/app/Helpers/HttpRequestHelper.php +++ b/app/Helpers/HttpRequestHelper.php @@ -15,6 +15,7 @@ class HttpRequestHelper $data = [ 'ip' => $request->getIPAddress(), + 'real_ip' => getRealClientIP(), 'platform' => $platform, 'browser' => $browser, 'method' => strtoupper($request->getMethod()), diff --git a/app/Helpers/utility_helper.php b/app/Helpers/utility_helper.php index 71eeb49..893a32d 100755 --- a/app/Helpers/utility_helper.php +++ b/app/Helpers/utility_helper.php @@ -773,19 +773,104 @@ function getRealClientIP() return $request->getIPAddress(); } -function generateFingerprint() +function generateFingerprint(): string { $request = service('request'); $ua = $request->getUserAgent()->getAgentString(); + // echo $ua; + // die; $ip = getRealClientIP(); - // echo $ip;die(); - // Use only subnet (first 3 blocks) to tolerate IP change - $ipParts = explode('.', $ip); - $ipSubnet = $ipParts[0] . '.' . $ipParts[1] . '.' . $ipParts[2]; - // $secret = env('app.sessionFingerprintSalt'); + // Normalize localhost + if ($ip === '127.0.0.1' || $ip === '::1') { + $ipGroup = 'localhost'; + } + // IPv4 handling + elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $parts = explode('.', $ip); + // Use /24 subnet (first 3 octets) + $ipGroup = $parts[0] . '.' . $parts[1] . '.' . $parts[2]; + } + // IPv6 handling + elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + // Use first 4 blocks of IPv6 (rough /64 grouping) + $blocks = explode(':', $ip); + $ipGroup = implode(':', array_slice($blocks, 0, 4)); + } + // Fallback + else { + $ipGroup = 'unknown'; + } - // return hash('sha256', $ua . '|' . $ipSubnet . '|' . $secret); - return hash('sha256', $ua . '|' . $ipSubnet ); -} \ No newline at end of file + // return $ua . '_' . $ipGroup; + return hash('sha256', $ua . '|' . $ipGroup); +} + + +/** + * Extract identity from POST body or GET params. + * Looks for 'email' or 'mobile_number'. + */ + function resolveIdentity($request): ?string + { + // Try POST body first + $email = $request->getPost('email'); + // print_r($email);die; + $mobile = $request->getPost('mobile_number'); + + // Fallback to GET params + if (! $email && ! $mobile) { + $email = $request->getGet('email'); + $mobile = $request->getGet('mobile_number'); + } + // Fallback to JSON params + if (! $email && ! $mobile) { + $req_data = $request->getJSON(); + // print_r( $req_data); + + $mobile = $req_data->mobile_number ?? null; + // return trim($mobile_number); + + $email = $req_data->email ?? null; + + if (!$email) + { + $email = $req_data->email_id ?? null; + } + // return trim($email); + } + + if ($email) { + return strtolower(trim($email)); + } + + if ($mobile) { + return trim($mobile); + } + + return null; + } + + + function recordRateLimitFailure(string $context = 'authApi'): void + { + /** @var IncomingRequest $request */ + $request = \Config\Services::request(); + + $limiter = \Config\Services::limiter(); // or your custom limiter service + + $fingerprint = $request->getVar('rateLimitFingerprint') + ?? generateFingerprint(); + + $identity = $request->getVar('rateLimitIdentity') + ?? resolveIdentity($request); + + // Record IP-level failure + $limiter->recordIpFailure($fingerprint); + + // Record user-level failure + if (!empty($identity)) { + $limiter->recordUserFailure($identity, $context); + } + } \ No newline at end of file diff --git a/app/Libraries/RateLimiterService.php b/app/Libraries/RateLimiterService.php new file mode 100644 index 0000000..a5120f5 --- /dev/null +++ b/app/Libraries/RateLimiterService.php @@ -0,0 +1,385 @@ +config = config('RateLimiter'); + $this->cache = \Config\Services::cache(); + } + + // ========================================================================= + // PUBLIC — IP LEVEL + // ========================================================================= + + /** + * Check & throttle by fingerprint (IP+UA based). + * Returns null on pass, or an array ['level'=>..., 'message'=>...] on block. + */ + public function checkIp(string $fingerprint, string $routeType = 'jwtApi'): ?array + { + // echo $fingerprint;die; + // 1. Is the IP already blocked? + $blockInfo = $this->getIpBlock($fingerprint); + // print_rr($blockInfo);die(); + if ($blockInfo) { + // Count hit while blocked → maybe escalate + $this->recordIpBlockHit($fingerprint, $blockInfo['level']); + return $this->blockedResponse('ip', $blockInfo['level']); + } + + // 2. Throttle check + $cfg = $this->config->ipBlock; + $countKey = $this->config->cacheKeys['ip_count'] . $fingerprint; + $count = (int) ($this->cache->get($countKey) ?? 0); + + if ($count === 0) { + $this->cache->save($countKey, 1, $cfg['window']); + } else { + $this->cache->save($countKey, $count + 1, $cfg['window']); + } + + if (($count + 1) > $cfg['limit']) { + // Over limit → record violation + $violated = $this->incrementIpViolation($fingerprint); + if ($violated >= $cfg['violation_soft']) { + $this->blockIp($fingerprint, 'soft'); + return $this->blockedResponse('ip', 'soft'); + } + return [ + 'level' => 'throttle', + 'message' => 'Too many requests. Please slow down.', + 'status' => $this->config->statusCodes['throttle'], + ]; + } + + return null; + } + + /** + * Record a "bad outcome" for IP (e.g. controller calls this after failed auth). + * Same escalation path as throttle violations. + */ + public function recordIpFailure(string $fingerprint): ?array + { + $blockInfo = $this->getIpBlock($fingerprint); + if ($blockInfo) { + $this->recordIpBlockHit($fingerprint, $blockInfo['level']); + return $this->blockedResponse('ip', $blockInfo['level']); + } + + $violated = $this->incrementIpViolation($fingerprint); + $cfg = $this->config->ipBlock; + + if ($violated >= $cfg['violation_soft']) { + $this->blockIp($fingerprint, 'soft'); + return $this->blockedResponse('ip', 'soft'); + } + + return null; + } + + /** + * Manually block an IP at a given level. + */ + public function blockIp(string $fingerprint, string $level = 'soft'): void + { + $cfg = $this->config->ipBlock; + $blockKey = $this->config->cacheKeys['ip_block'] . $fingerprint; + + $duration = $this->blockDuration($cfg, $level); + + $data = [ + 'level' => $level, + 'blocked_at' => time(), + 'fingerprint'=> $fingerprint, + ]; + + // Duration 0 = store for 10 years (permanent until manual unblock) + $ttl = $duration > 0 ? $duration : (10 * 365 * 24 * 3600); + $this->cache->save($blockKey, $data, $ttl); + } + + /** + * Manually unblock an IP. Clears block, violations, and counters. + */ + public function unblockIp(string $fingerprint): void + { + $keys = $this->config->cacheKeys; + $this->cache->delete($keys['ip_block'] . $fingerprint); + $this->cache->delete($keys['ip_violations'] . $fingerprint); + $this->cache->delete($keys['ip_count'] . $fingerprint); + $this->cache->delete($keys['ip_block_hits'] . $fingerprint); + } + + /** + * Get current IP block info or null if not blocked. + */ + public function getIpBlock(string $fingerprint): ?array + { + $blockKey = $this->config->cacheKeys['ip_block'] . $fingerprint; + $data = $this->cache->get($blockKey); + return $data ?: null; + } + + // ========================================================================= + // PUBLIC — USER LEVEL + // ========================================================================= + + /** + * Check if a user (by email or mobile) is blocked. + * Returns null on pass, or block response array on block. + */ + public function checkUser(string $identity): ?array + { + $blockInfo = $this->getUserBlock($identity); + if ($blockInfo) { + $this->recordUserBlockHit($identity, $blockInfo['level']); + return $this->blockedResponse('user', $blockInfo['level']); + } + return null; + } + + /** + * Record a failed attempt for a user identity. + * Called from controller after() or manually after a failed verification. + * Handles escalation: free → soft → medium → hard + */ + public function recordUserFailure(string $identity, string $routeType = 'authApi'): ?array + { + $blockInfo = $this->getUserBlock($identity); + + if ($blockInfo) { + // Already blocked — count hit and maybe escalate + $this->recordUserBlockHit($identity, $blockInfo['level']); + return $this->blockedResponse('user', $blockInfo['level']); + } + + // Not blocked yet — increment violation count + $violated = $this->incrementUserViolation($identity, $routeType); + $cfg = $this->config->userBlock; + $routeCfg = $this->config->{$routeType}; + + if ($violated >= $routeCfg['violation_soft']) { + $this->blockUser($identity, 'soft'); + return $this->blockedResponse('user', 'soft'); + } + + return null; + } + + /** + * Manually block a user identity at a given level. + */ + public function blockUser(string $identity, string $level = 'soft'): void + { + $cfg = $this->config->userBlock; + $blockKey = $this->config->cacheKeys['user_block'] . $this->hashIdentity($identity); + + $duration = $this->blockDuration($cfg, $level); + $ttl = $duration > 0 ? $duration : (10 * 365 * 24 * 3600); + + $data = [ + 'level' => $level, + 'blocked_at' => time(), + 'identity' => $identity, + ]; + + $this->cache->save($blockKey, $data, $ttl); + } + + /** + * Manually unblock a user identity. Independent — does NOT touch IP block. + */ + public function unblockUser(string $identity): void + { + $keys = $this->config->cacheKeys; + $hashed = $this->hashIdentity($identity); + + $this->cache->delete($keys['user_block'] . $hashed); + $this->cache->delete($keys['user_violations'] . $hashed); + $this->cache->delete($keys['user_count'] . $hashed); + $this->cache->delete($keys['user_block_hits'] . $hashed); + } + + /** + * Get current user block info or null if not blocked. + */ + public function getUserBlock(string $identity): ?array + { + $blockKey = $this->config->cacheKeys['user_block'] . $this->hashIdentity($identity); + $data = $this->cache->get($blockKey); + return $data ?: null; + } + + // ========================================================================= + // USER THROTTLE (for JWT API routes — request count based) + // ========================================================================= + + /** + * Throttle check for a known user on JWT routes. + * Increments request counter; if over limit records violation. + */ + public function checkUserThrottle(string $identity, string $routeType = 'jwtApi'): ?array + { + $blockCheck = $this->checkUser($identity); + if ($blockCheck) { + return $blockCheck; + } + + $cfg = $this->config->{$routeType}; + $hashed = $this->hashIdentity($identity); + $countKey = $this->config->cacheKeys['user_count'] . $hashed; + $count = (int) ($this->cache->get($countKey) ?? 0); + + if ($count === 0) { + $this->cache->save($countKey, 1, $cfg['window']); + } else { + $this->cache->save($countKey, $count + 1, $cfg['window']); + } + + if (($count + 1) > $cfg['limit']) { + $violated = $this->incrementUserViolation($identity, $routeType); + if ($violated >= $cfg['violation_soft']) { + $this->blockUser($identity, 'soft'); + return $this->blockedResponse('user', 'soft'); + } + return [ + 'level' => 'throttle', + 'message' => 'Too many requests. Please slow down.', + 'status' => $this->config->statusCodes['throttle'], + ]; + } + + return null; + } + + // ========================================================================= + // PRIVATE HELPERS + // ========================================================================= + + /** + * Increment IP violation counter and return new count. + */ + protected function incrementIpViolation(string $fingerprint): int + { + $key = $this->config->cacheKeys['ip_violations'] . $fingerprint; + $count = (int) ($this->cache->get($key) ?? 0) + 1; + // Keep violation record for the block window duration + $this->cache->save($key, $count, $this->config->ipBlock['window'] * 10); + return $count; + } + + /** + * Record a hit while IP is already blocked; escalate if thresholds met. + */ + protected function recordIpBlockHit(string $fingerprint, string $currentLevel): void + { + $cfg = $this->config->ipBlock; + $hitKey = $this->config->cacheKeys['ip_block_hits'] . $fingerprint; + $hits = (int) ($this->cache->get($hitKey) ?? 0) + 1; + $this->cache->save($hitKey, $hits, 10 * 365 * 24 * 3600); + + if ($currentLevel === 'soft' && $hits >= $cfg['medium_trigger']) { + $this->cache->delete($hitKey); + $this->blockIp($fingerprint, 'medium'); + } elseif ($currentLevel === 'medium' && $hits >= $cfg['hard_trigger']) { + $this->cache->delete($hitKey); + $this->blockIp($fingerprint, 'hard'); + } + } + + /** + * Increment user violation counter and return new count. + */ + protected function incrementUserViolation(string $identity, string $routeType): int + { + $hashed = $this->hashIdentity($identity); + $key = $this->config->cacheKeys['user_violations'] . $hashed; + $count = (int) ($this->cache->get($key) ?? 0) + 1; + $window = $this->config->{$routeType}['window'] ?? 180; + $this->cache->save($key, $count, $window * 10); + return $count; + } + + /** + * Record a hit while user is already blocked; escalate if thresholds met. + */ + protected function recordUserBlockHit(string $identity, string $currentLevel): void + { + $cfg = $this->config->userBlock; + $hashed = $this->hashIdentity($identity); + $hitKey = $this->config->cacheKeys['user_block_hits'] . $hashed; + $hits = (int) ($this->cache->get($hitKey) ?? 0) + 1; + $this->cache->save($hitKey, $hits, 10 * 365 * 24 * 3600); + + if ($currentLevel === 'soft' && $hits >= $cfg['medium_trigger']) { + $this->cache->delete($hitKey); + $this->blockUser($identity, 'medium'); + } elseif ($currentLevel === 'medium' && $hits >= $cfg['hard_trigger']) { + $this->cache->delete($hitKey); + $this->blockUser($identity, 'hard'); + } + } + + /** + * Resolve block duration from config based on level. + */ + protected function blockDuration(array $cfg, string $level): int + { + return match ($level) { + 'soft' => $cfg['soft_duration'], + 'medium' => $cfg['medium_duration'], + 'hard' => $cfg['hard_duration'], + default => 0, + }; + } + + /** + * Build a standardised blocked response array. + */ + protected function blockedResponse(string $type, string $level): array + { + $messages = [ + 'soft' => 'Your access has been temporarily suspended. Please contact support.', + 'medium' => 'Your access has been restricted due to repeated violations.', + 'hard' => 'Your access has been permanently blocked. Please contact support.', + ]; + + return [ + 'level' => $level, + 'type' => $type, + 'message' => $messages[$level] ?? 'Access denied.', + 'status' => $this->config->statusCodes[$level], + ]; + } + + /** + * Hash user identity (email or mobile) for cache key safety. + */ + protected function hashIdentity(string $identity): string + { + // return strtolower(trim($identity)); + return hash('sha256', strtolower(trim($identity))); + } +}