From 5cd6cfb7fad58df0c1145cb7a460469a0af35b13 Mon Sep 17 00:00:00 2001 From: velz Date: Thu, 8 Jan 2026 16:02:00 +0530 Subject: [PATCH 01/15] FEAT_SANITIZATION --- app/Config/Filters.php | 3 + app/Filters/Cors.php | 66 +++++++------- app/Filters/SecurityInputFilter.php | 130 ++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 33 deletions(-) create mode 100644 app/Filters/SecurityInputFilter.php diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 54e8396..969dadf 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -17,6 +17,7 @@ use App\Filters\VerifyAppSignature; use App\Filters\AuthJWT; use App\Filters\Cors; use App\Filters\GlobalPostFileUploadGuard; +use App\Filters\SecurityInputFilter; class Filters extends BaseConfig { @@ -41,6 +42,7 @@ class Filters extends BaseConfig 'Cors' => Cors::class, 'appSignature' => VerifyAppSignature::class, 'GlobalPostFileUploadGuard' => GlobalPostFileUploadGuard::class, + 'SecurityInputFilter' => SecurityInputFilter::class, ]; @@ -55,6 +57,7 @@ class Filters extends BaseConfig 'before' => [ 'HttpRequestLog' => ['except' => 'cli/*'], 'Cors', + 'SecurityInputFilter', 'GlobalPostFileUploadGuard', // 'invalidchars', ], diff --git a/app/Filters/Cors.php b/app/Filters/Cors.php index c47f276..bb8d5b8 100644 --- a/app/Filters/Cors.php +++ b/app/Filters/Cors.php @@ -107,11 +107,11 @@ class Cors implements FilterInterface ); } - $this->log('CORS filter initialized', [ - 'allowed_origins' => $this->allowedOrigins, - 'allow_credentials' => $this->allowCredentials, - 'allowed_methods' => $this->allowedMethods, - ]); + // $this->log('CORS filter initialized', [ + // 'allowed_origins' => $this->allowedOrigins, + // 'allow_credentials' => $this->allowCredentials, + // 'allowed_methods' => $this->allowedMethods, + // ]); } /** @@ -143,7 +143,7 @@ class Cors implements FilterInterface // If wildcard present in configuration, allow any origin if (in_array('*', $this->allowedOrigins, true)) { - $this->log('Origin allowed: wildcard match', ['origin' => $origin]); + // $this->log('Origin allowed: wildcard match', ['origin' => $origin]); return true; } @@ -159,10 +159,10 @@ class Cors implements FilterInterface // 1. Exact match (including scheme and port) // Example: https://example.com matches https://example.com if (strcasecmp($allowed, $origin) === 0) { - $this->log('Origin allowed: exact match', [ - 'origin' => $origin, - 'matched_rule' => $allowed - ]); + // $this->log('Origin allowed: exact match', [ + // 'origin' => $origin, + // 'matched_rule' => $allowed + // ]); return true; } @@ -178,11 +178,11 @@ class Cors implements FilterInterface // Check if origin host ends with the allowed root domain if ($originHost === $allowedRoot || str_ends_with($originHost, '.' . $allowedRoot)) { - $this->log('Origin allowed: wildcard subdomain match', [ - 'origin' => $origin, - 'matched_rule' => $allowed, - 'origin_host' => $originHost - ]); + // $this->log('Origin allowed: wildcard subdomain match', [ + // 'origin' => $origin, + // 'matched_rule' => $allowed, + // 'origin_host' => $originHost + // ]); return true; } } @@ -191,11 +191,11 @@ class Cors implements FilterInterface // Example: example.com matches both http://example.com and https://example.com else { if (strcasecmp($allowed, $originHost) === 0) { - $this->log('Origin allowed: host match (scheme-less)', [ - 'origin' => $origin, - 'matched_rule' => $allowed, - 'origin_host' => $originHost - ]); + // $this->log('Origin allowed: host match (scheme-less)', [ + // 'origin' => $origin, + // 'matched_rule' => $allowed, + // 'origin_host' => $originHost + // ]); return true; } } @@ -320,11 +320,11 @@ class Cors implements FilterInterface // Preflight is sent by browsers before actual cross-origin requests // to check if the actual request is safe to send if ($method === 'OPTIONS') { - $this->log('Preflight request received', [ - 'origin' => $origin, - 'method' => $method, - 'uri' => (string) $request->getUri() - ]); + // $this->log('Preflight request received', [ + // 'origin' => $origin, + // 'method' => $method, + // 'uri' => (string) $request->getUri() + // ]); // Validate origin - reject if not allowed if (empty($origin) || !$this->isOriginAllowed($origin)) { @@ -346,10 +346,10 @@ class Cors implements FilterInterface $response->setStatusCode(204); $response->setBody(''); - $this->log('Preflight approved', [ - 'origin' => $origin, - 'allowed_methods' => $this->allowedMethods - ]); + // $this->log('Preflight approved', [ + // 'origin' => $origin, + // 'allowed_methods' => $this->allowedMethods + // ]); return $response; } @@ -392,10 +392,10 @@ class Cors implements FilterInterface // Add CORS headers to the response $this->addCorsHeaders($response, $request, $origin, false); - $this->log('CORS headers added to response', [ - 'origin' => $origin, - 'status' => $response->getStatusCode() - ]); + // $this->log('CORS headers added to response', [ + // 'origin' => $origin, + // 'status' => $response->getStatusCode() + // ]); } /** diff --git a/app/Filters/SecurityInputFilter.php b/app/Filters/SecurityInputFilter.php new file mode 100644 index 0000000..c23f790 --- /dev/null +++ b/app/Filters/SecurityInputFilter.php @@ -0,0 +1,130 @@ +/i', + + // JavaScript execution vectors + '/javascript\s*:/i', + '/vbscript\s*:/i', + '/data\s*:\s*text\/html/i', + + // Inline event handlers (strong signal) + '/on\w+\s*=\s*["\']?/i', + + // Dangerous HTML tags + '/<\s*iframe\b/i', + '/<\s*object\b/i', + '/<\s*embed\b/i', + '/<\s*applet\b/i', + '/<\s*img\b/i', + + // Image-based execution + '/<\s*img\b[^>]*on\w+/i', + + // SVG-based execution (modern bypass) + '/<\s*svg\b/i', + '/<\s*math\b/i', + + // Meta refresh redirect + '/<\s*meta\b[^>]*http-equiv\s*=\s*["\']?refresh/i', + + // HTML injection via src/href + '/<\s*\w+\b[^>]*(src|href)\s*=\s*["\']?\s*(javascript|data)\s*:/i' + ]; + + + public function before(RequestInterface $request, $arguments = null) + { + $logger = Services::mylogger(); + $response = Services::response(); + + // Collect all user-controlled input + $inputs = array_merge( + $request->getGet(), + $request->getPost() + ); + + if (empty($inputs)) { + return; + } + + foreach ($inputs as $field => $value) { + if (is_array($value)) { + $value = json_encode($value); + } + + // Step 1: Canonicalization (VERY IMPORTANT) + $canonical = $this->canonicalize($value); + + // Step 2: Trim (hygiene) + $canonical = trim($canonical); + + // Step 3: Detection (signal-only) + if ($this->detectXss($canonical)) { + + // 🔐 Log intent, not data + $logger->logme('critical','SECURITY_BLOCKED_REQUEST - '. json_encode([ + 'ip' => $request->getIPAddress(), + 'method' => $request->getMethod(), + 'uri' => current_url(), + 'field' => $field, + 'attack' => 'XSS_PATTERN', + 'length' => strlen($canonical), + 'hash' => hash('sha256', $canonical), + ])); + + // ⛔ Block request + return $response + ->setStatusCode(403) + ->setJSON([ + 'status' => 403, + 'error' => 'Forbidden', + 'message' => 'Malicious input detected' + ]); + } + } + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + // no-op + } + + /** + * Canonicalization prevents encoded bypass + */ + private function canonicalize(string $value): string + { + $value = urldecode($value); + $value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // Remove invisible control characters + return preg_replace('/[\x00-\x1F\x7F]/u', '', $value); + } + + private function detectXss(string $value): bool + { + foreach ($this->xssPatterns as $pattern) { + if (preg_match($pattern, $value)) { + return true; + } + } + return false; + } +} From 574f9963d10d2ddb7253743af6c5361e3141cef0 Mon Sep 17 00:00:00 2001 From: velz Date: Thu, 8 Jan 2026 16:37:26 +0530 Subject: [PATCH 02/15] FIX_SESSION_REPLAY --- app/Controllers/LoginController.php | 31 ++++++++++++--------- app/Filters/AuthMVC.php | 21 +++++++++++++-- app/Helpers/session_helper.php | 2 +- app/Libraries/AuthLogout.php | 42 +++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 app/Libraries/AuthLogout.php diff --git a/app/Controllers/LoginController.php b/app/Controllers/LoginController.php index 830fb02..eb3d98e 100755 --- a/app/Controllers/LoginController.php +++ b/app/Controllers/LoginController.php @@ -9,6 +9,7 @@ use Psr\Log\LoggerInterface; use App\Models\UserModel; use App\Models\AuthHistoryModel; +use App\Libraries\AuthLogout; class LoginController extends BaseController { @@ -54,25 +55,29 @@ class LoginController extends BaseController $path = getenv('cookie.Path'); $domain = getenv('cookie.Domain'); $https = getenv('ccokie.secure'); - setcookie('session_data', json_encode($session_data), time() + 12 * 60 * 60, $path, $domain, $https, true); + // setcookie('session_data', json_encode($session_data), time() + 12 * 60 * 60, $path, $domain, $https, true); set_session_data($session_data); + // Bind session to device + set_session_data(['fingerprint' => hash('sha256', + ($this->request->getUserAgent()->getAgentString() . '|' . ($this->request->getIPAddress() + )))]); log_message('error', 'Set The UserId : `'. $user->id .'` in Session'); log_message('error', 'User Login Sucessfully'); $this->getUserDeviceInfo($user->id, 'NhanceUser'); - return redirect()->to(base_url('/dashboard/view')); + return AuthLogout::logout(); }else{ log_message('error', 'User Not Active'); session()->setFlashdata('error', 'User Not Active'); - return redirect()->to(base_url('login')); + return AuthLogout::logout(); } }else{ log_message('error', 'User Not Registered'); session()->setFlashdata('error', 'User Not Registered'); - return redirect()->to(base_url('login')); + return AuthLogout::logout(); } } @@ -94,14 +99,16 @@ class LoginController extends BaseController // setcookie('session_data', '', time() - 3600, $path); // return redirect()->to(base_url('login')); - $path = getenv('cookie.Path'); - session()->destroy(); - // setcookie('session_data', '', time() - 3600, $path); - $path = getenv('cookie.Path'); - $domain = getenv('cookie.Domain'); - $https = getenv('cookie.secure'); - setcookie('session_data',null, time() -3600, $path, $domain, $https, true); - return redirect()->to(base_url('login')); + // $path = getenv('cookie.Path'); + // session()->destroy(); + // // setcookie('session_data', '', time() - 3600, $path); + // $path = getenv('cookie.Path'); + // $domain = getenv('cookie.Domain'); + // $https = getenv('cookie.secure'); + // setcookie('session_data',null, time() -3600, $path, $domain, $https, true); + // return redirect()->to(base_url('login')); + + return AuthLogout::logout(); } diff --git a/app/Filters/AuthMVC.php b/app/Filters/AuthMVC.php index 0a9f7a6..e6c6948 100755 --- a/app/Filters/AuthMVC.php +++ b/app/Filters/AuthMVC.php @@ -5,12 +5,29 @@ use CodeIgniter\Filters\FilterInterface; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; +use App\Libraries\AuthLogout; + class AuthMVC implements FilterInterface { public function before(RequestInterface $request, $arguments = null) { - if (!check_session() && !check_cookie()) { - return redirect()->to(base_url('/login')); + if (!check_session()) + { + return AuthLogout::logout(); + } + + // if (!check_cookie()) + // { + // return AuthLogout::logout(); + // } + + // Fingerprint validation + $fp = hash('sha256', + $request->getUserAgent()->getAgentString() . '|' . $request->getIPAddress() + ); + + if (session()->get('fingerprint') !== $fp) { + return AuthLogout::logout(); } } diff --git a/app/Helpers/session_helper.php b/app/Helpers/session_helper.php index 3d312d6..eb9eb9a 100755 --- a/app/Helpers/session_helper.php +++ b/app/Helpers/session_helper.php @@ -21,7 +21,7 @@ if(!function_exists('check_cookie')){ 'userProfile' => $value['userProfile'], 'user_team' => $user_team, ]; - set_session_data($session_data); + // set_session_data($session_data); // $this->getUserDeviceInfo($user->id, 'NhanceUser'); // return redirect()->to(base_url('/dashboard/view')); return true; diff --git a/app/Libraries/AuthLogout.php b/app/Libraries/AuthLogout.php new file mode 100644 index 0000000..cfb1005 --- /dev/null +++ b/app/Libraries/AuthLogout.php @@ -0,0 +1,42 @@ +regenerate(true); + + // Destroy CI session + $session->destroy(); + + // Kill PHP session cookie safely + if (ini_get('session.use_cookies')) { + $params = session_get_cookie_params(); + + setcookie( + session_name(), // DO NOT hardcode cookie name + '', + time() - 42000, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); + } + + session_write_close(); + + // Redirect with anti-cache headers + return redirect()->to(base_url('login')) + ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->setHeader('Pragma', 'no-cache') + ->setHeader('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT'); + } +} From 4d15ddb92a1f9e029595049948a959071007e3ff Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Thu, 8 Jan 2026 16:58:50 +0530 Subject: [PATCH 03/15] GWM : VAPT Scan Vulnerabilities --- app/Config/Routes.php | 2 + .../RestAuthenticationController.php | 81 ++++++++- app/Filters/AuthJWT.php | 160 ++++++++++++------ app/Helpers/JWTToken.php | 93 +++++++--- app/Helpers/utility_helper.php | 26 +++ 5 files changed, 285 insertions(+), 77 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index eaf7741..6524071 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -455,6 +455,8 @@ $routes->group("/api", ["filter" => "authJWT"], function ($routes) { $routes->group("employeeRest", ['filter' => ['appSignature' , 'authJWT'] ], function ($routes) { + $routes->post('logout', 'RestAuthenticationController::logout'); + // $routes->post("getVerifiedUserData", "RestAuthenticationController::getVerifiedUserData"); $routes->post("storeFireBase", "EmployeeRestController::storeFireBase"); diff --git a/app/Controllers/RestAuthenticationController.php b/app/Controllers/RestAuthenticationController.php index 22e834e..15469ec 100755 --- a/app/Controllers/RestAuthenticationController.php +++ b/app/Controllers/RestAuthenticationController.php @@ -166,7 +166,14 @@ class RestAuthenticationController extends AdminController $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithMobileNumber: Final employee data to verify = " . json_encode($employeeData)); if (isset($employeeData['employee_id'])) - { + { + + //check the resend otp (with in 60 seconds don't allow another otp to send) + $check = canSendOtp($employeeData); + if (!$check['allowed']) { + return $this->respond(['status' => false,'message' => 'OTP already sent. Please wait before retrying.','retry_after_seconds' => $check['retry_after'] ])->setStatusCode(429); // Too Many Requests + } + $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithMobileNumber: Employee verified with ID = " . $employeeData['employee_id']); log_message('error', ' '); log_message('error', '************************ PRE END ********************************'); @@ -264,10 +271,11 @@ class RestAuthenticationController extends AdminController $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithEmailId: Received Email ID = " . $email); $empdata = RestAuthHelper::getPreAndPostDataByEmailOrMobile(['email_id' => $email]); - // print_r($empdata); die; + $otp = random_int(100000, 999999); + //only retail policy if(empty($empdata)){ $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithEmailId: No employee data found both PRE & POST"); log_message('error', ' '); @@ -323,8 +331,15 @@ class RestAuthenticationController extends AdminController $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithEmailId: Using PRE data"); } + + if (isset($employeeData['employee_id'])) { + //check the resend otp (with in 60 seconds don't allow another otp to send) + $check = canSendOtp($employeeData); + if (!$check['allowed']) { + return $this->respond(['status' => false,'message' => 'OTP already sent. Please wait before retrying.','retry_after_seconds' => $check['retry_after'] ])->setStatusCode(429); // Too Many Requests + } $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - verifyEmployeeWithEmailId: Valid employee found, generating OTP"); @@ -578,6 +593,12 @@ class RestAuthenticationController extends AdminController if ($HrData) { + + //check the resend otp (with in 60 seconds don't allow another otp to send) + $check = canSendOtp($HrData); + if (!$check['allowed']) { + return $this->respond(['status' => false,'message' => 'OTP already sent. Please wait before retrying.','retry_after_seconds' => $check['retry_after'] ])->setStatusCode(429); // Too Many Requests + } $sql = "UPDATE level_contacts SET otp = ? WHERE mobile = ? AND contact_type = 'client' AND is_active = 1"; @@ -637,6 +658,12 @@ class RestAuthenticationController extends AdminController $data->otp = $otp; if ($HrData) { + + //check the resend otp (with in 60 seconds don't allow another otp to send) + $check = canSendOtp($HrData); + if (!$check['allowed']) { + return $this->respond(['status' => false,'message' => 'OTP already sent. Please wait before retrying.','retry_after_seconds' => $check['retry_after'] ])->setStatusCode(429); // Too Many Requests + } @@ -1868,5 +1895,55 @@ class RestAuthenticationController extends AdminController } } + + public function logout() + { + $authHeader = $this->request->getHeaderLine('Authorization'); + + if (!$authHeader) { + return $this->respond([ + 'status' => false, + 'message' => 'Authorization token missing' + ], 401); + } + + // Validate JWT (your hardened function) + $result = JWTToken::validateJWT($authHeader); + + if ($result['status'] !== true) { + return $this->respond([ + 'status' => false, + 'message' => 'Invalid or expired token' + ], 401); + } + + $decoded = $result['decoded']; + $userId = $decoded['id'] ?? null; + + if (!$userId) { + return $this->respond([ + 'status' => false, + 'message' => 'Invalid token payload' + ], 401); + } + + // Identify user type + if (isset($decoded['emp_code'])) { + $model = new EmployeeModel(); + } else { + $model = new LevelContactModel(); + } + + // Invalidate token server-side + $model->update($userId, [ + 'token_time_out' => null + ]); + + return $this->respond([ + 'status' => true, + 'message' => 'Logged out successfully' + ], 200); + } + } diff --git a/app/Filters/AuthJWT.php b/app/Filters/AuthJWT.php index 262a5e5..9530ebd 100755 --- a/app/Filters/AuthJWT.php +++ b/app/Filters/AuthJWT.php @@ -18,65 +18,115 @@ use App\Models\LevelContactModel; class AuthJWT implements FilterInterface { + // public function before(RequestInterface $request, $arguments = null) + // { + // $jwt = $request->getHeaderLine('Authorization'); + + // if ($jwt) { + // if (JWTToken::validateJWT($jwt)) { + // $data = JWTToken::validateJWT($jwt); + // $data = json_decode($data); + + // $id = $data->decoded->id; + // if(isset($data->decoded->emp_code)){ + // $model = new EmployeeModel(); + // $user_data = $model->where('id', $id)->first(); + + // if($user_data['token_time_out'] > time()){ + // $data =["token_time_out" => time() + getenv('TOKENTIMEOUT') ]; + // $model->update($id, $data); + // return true; + // }else{ + + // // if($user_data['token_time_out'] != "" && $user_data['token_time_out'] != NULL) + + // $data =["token_time_out" => '']; + // $model->update($id, $data); + // header('Content-Type: application/json'); + // http_response_code(401); + // // $error = json_encode(["status" => 401, "message" => $data->message]); + // $error = json_encode(["status" => 401, "message" => "Token is Invalid"]); + // echo $error; + // exit; + // } + // }else{ + // $model = new LevelContactModel(); + // $hr_data = $model->where('id', $id)->first(); + + // if($hr_data['token_time_out'] > time()){ + // $data =["token_time_out" => time() + getenv('TOKENTIMEOUT') ]; + // $model->update($id, $data); + // return true; + // }else{ + // $data =["token_time_out" => '']; + // $model->update($id, $data); + // header('Content-Type: application/json'); + // http_response_code(401); + // // $error = json_encode(["status" => 401, "message" => $data->message]); + // $error = json_encode(["status" => 401, "message" => "Token is Invalid"]); + // echo $error; + // exit; + // } + // } + + // } + // } else { + // header('Content-Type: application/json'); + // http_response_code(403); + // $error = json_encode(["status" => 403, "message" => "Access Forbidden!"]); + // echo $error; + // exit(); + // } + // } + public function before(RequestInterface $request, $arguments = null) { - $jwt = $request->getHeaderLine('Authorization'); + $authHeader = $request->getHeaderLine('Authorization'); - if ($jwt) { - if (JWTToken::validateJWT($jwt)) { - $data = JWTToken::validateJWT($jwt); - $data = json_decode($data); - - $id = $data->decoded->id; - if(isset($data->decoded->emp_code)){ - $model = new EmployeeModel(); - $user_data = $model->where('id', $id)->first(); - - if($user_data['token_time_out'] > time()){ - $data =["token_time_out" => time() + getenv('TOKENTIMEOUT') ]; - $model->update($id, $data); - return true; - }else{ - - // if($user_data['token_time_out'] != "" && $user_data['token_time_out'] != NULL) - - $data =["token_time_out" => '']; - $model->update($id, $data); - header('Content-Type: application/json'); - http_response_code(401); - // $error = json_encode(["status" => 401, "message" => $data->message]); - $error = json_encode(["status" => 401, "message" => "Token is Invalid"]); - echo $error; - exit; - } - }else{ - $model = new LevelContactModel(); - $hr_data = $model->where('id', $id)->first(); - - if($hr_data['token_time_out'] > time()){ - $data =["token_time_out" => time() + getenv('TOKENTIMEOUT') ]; - $model->update($id, $data); - return true; - }else{ - $data =["token_time_out" => '']; - $model->update($id, $data); - header('Content-Type: application/json'); - http_response_code(401); - // $error = json_encode(["status" => 401, "message" => $data->message]); - $error = json_encode(["status" => 401, "message" => "Token is Invalid"]); - echo $error; - exit; - } - } - - } - } else { - header('Content-Type: application/json'); - http_response_code(403); - $error = json_encode(["status" => 403, "message" => "Access Forbidden!"]); - echo $error; - exit(); + if (!$authHeader) { + return $this->reject(403, 'Access Forbidden'); } + + $result = JWTToken::validateJWT($authHeader); + + if ($result['status'] !== true) { + return $this->reject(401, $result['message']); + } + + $decoded = $result['decoded']; + $id = $decoded['id'] ?? null; + + if (!$id) { + return $this->reject(401, 'Invalid token payload'); + } + + if (isset($decoded['emp_code'])) { + $model = new EmployeeModel(); + } else { + $model = new LevelContactModel(); + } + + $user = $model->find($id); + + if (!$user || $user['token_time_out'] <= time()) { + $model->update($id, ['token_time_out' => null]); + return $this->reject(401, 'Token expired'); + } + + // Refresh sliding expiration + $model->update($id, [ + 'token_time_out' => time() + getenv('TOKENTIMEOUT') + ]); + + return true; + } + + private function reject(int $code, string $message) + { + return service('response') + ->setStatusCode($code) + ->setJSON(['status' => $code, 'message' => $message]) + ->send(); } public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) diff --git a/app/Helpers/JWTToken.php b/app/Helpers/JWTToken.php index 41d76b5..62b7281 100755 --- a/app/Helpers/JWTToken.php +++ b/app/Helpers/JWTToken.php @@ -18,9 +18,13 @@ use App\Models\LevelContactModel; class JWTToken { + + private const ALLOWED_ALG = 'HS512'; + + public static function encode($data =null) { - $secret_Key ="secret"; + $secret_Key = env('JWT_SECRET'); $request_data = (array)$data; @@ -47,30 +51,79 @@ class JWTToken } } - public static function validateJWT($jwt) + // public static function validateJWT($jwt) + // { + // $jwtParts = explode(' ', $jwt); + + // // print_r($jwtParts); + // if (count($jwtParts) != 2 || $jwtParts[0] == 'Bearer') { + // return false; + // } + + // $token = $jwtParts[1]; + + // try { + // $decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS512')); + // return json_encode(['status' => true, 'message' => 'Token is valid', 'decoded' => (array) $decoded]); + // } catch (ExpiredException $e) { + // return json_encode(['status' => false, 'message' => 'Token has expired']); + // } catch (BeforeValidException $e) { + // return json_encode(['status' => false, 'message' => 'Token is not yet valid']); + // } catch (SignatureInvalidException $e) { + // return json_encode(['status' => false, 'message' => 'Token signature is invalid']); + // } catch (\Exception $e) { + // return json_encode(['status' => false, 'message' => 'An error occurred while decoding the token']); + // } + + // } + + public static function validateJWT(string $authHeader) { - $jwtParts = explode(' ', $jwt); + // 1️⃣ Validate Authorization header + if (!preg_match('/^Bearer\s(\S+)$/', $authHeader, $matches)) { + return ['status' => false, 'message' => 'Invalid Authorization header']; + } - // print_r($jwtParts); - if (count($jwtParts) != 2 || $jwtParts[0] == 'Bearer') { - return false; - } + $token = $matches[1]; - $token = $jwtParts[1]; + // 2️⃣ Decode JWT header manually + $jwtParts = explode('.', $token); + if (count($jwtParts) !== 3) { + return ['status' => false, 'message' => 'Malformed JWT']; + } - try { - $decoded = JWT::decode($token, new Key("secret", 'HS512')); - return json_encode(['status' => true, 'message' => 'Token is valid', 'decoded' => (array) $decoded]); - } catch (ExpiredException $e) { - return json_encode(['status' => false, 'message' => 'Token has expired']); - } catch (BeforeValidException $e) { - return json_encode(['status' => false, 'message' => 'Token is not yet valid']); - } catch (SignatureInvalidException $e) { - return json_encode(['status' => false, 'message' => 'Token signature is invalid']); - } catch (\Exception $e) { - return json_encode(['status' => false, 'message' => 'An error occurred while decoding the token']); - } + $header = json_decode(base64_decode(strtr($jwtParts[0], '-_', '+/')), true); + // 3️⃣ Reject missing or NONE algorithm + if ( + empty($header['alg']) || + $header['alg'] === 'none' || + $header['alg'] !== self::ALLOWED_ALG + ) { + return ['status' => false, 'message' => 'Invalid or unsupported JWT algorithm']; + } + + // 4️⃣ Enforce signature validation + try { + $decoded = JWT::decode( + $token, + new Key(env('JWT_SECRET'), self::ALLOWED_ALG) + ); + + return [ + 'status' => true, + 'decoded' => (array) $decoded + ]; + + } catch (ExpiredException $e) { + return ['status' => false, 'message' => 'Token expired']; + } catch (BeforeValidException $e) { + return ['status' => false, 'message' => 'Token not yet valid']; + } catch (SignatureInvalidException $e) { + return ['status' => false, 'message' => 'Invalid token signature']; + } catch (\Exception $e) { + return ['status' => false, 'message' => 'Token validation failed']; + } } diff --git a/app/Helpers/utility_helper.php b/app/Helpers/utility_helper.php index 3372200..5ddf424 100755 --- a/app/Helpers/utility_helper.php +++ b/app/Helpers/utility_helper.php @@ -666,3 +666,29 @@ if (!function_exists('check_pay_by_employee_or_company')) { } +if (!function_exists('canSendOtp')) { + function canSendOtp(array $row, int $limitSeconds = 60): array + { + // If OTP does not exist → allow + if (empty($row['otp']) || empty($row['updated_at'])) { + return ['allowed' => true]; + } + + $lastUpdated = strtotime($row['updated_at']); + $currentTime = time(); + + // Calculate expiry time + $allowedAfter = $lastUpdated + $limitSeconds; + + // If still within limit → block + if ($currentTime < $allowedAfter) { + return [ + 'allowed' => false, + 'retry_after' => $allowedAfter - $currentTime + ]; + } + + return ['allowed' => true]; + } +} + From 42bd609ad8ee8ef9262b85f0e357170b4f48df9d Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Thu, 8 Jan 2026 18:04:11 +0530 Subject: [PATCH 04/15] GWM : token expiry issue in hr --- app/Filters/AuthJWT.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Filters/AuthJWT.php b/app/Filters/AuthJWT.php index 9530ebd..4eac9e5 100755 --- a/app/Filters/AuthJWT.php +++ b/app/Filters/AuthJWT.php @@ -104,6 +104,7 @@ class AuthJWT implements FilterInterface $model = new EmployeeModel(); } else { $model = new LevelContactModel(); + $id = $decoded['pre_hr_id'] ?? null; } $user = $model->find($id); From 375d0b132e0d180877aec665e77087aca0359bd9 Mon Sep 17 00:00:00 2001 From: velz Date: Fri, 9 Jan 2026 16:02:46 +0530 Subject: [PATCH 05/15] FEAT_RBAC --- .env.sample | 2 + .htaccess | 2 +- app/Config/Acl.php | 85 +++++++++++++++ app/Config/Filters.php | 5 + app/Controllers/LoginController.php | 8 +- app/Filters/AclFilter.php | 156 ++++++++++++++++++++++++++++ app/Libraries/AuthLogout.php | 2 +- 7 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 app/Config/Acl.php create mode 100644 app/Filters/AclFilter.php diff --git a/.env.sample b/.env.sample index 36880c7..c302d13 100644 --- a/.env.sample +++ b/.env.sample @@ -183,3 +183,5 @@ CORS_MAX_AGE=7200 CORS_DEBUG=true APP_SIGNATURE = +TOKENTIMEOUT = +JWT_SECRET = \ No newline at end of file diff --git a/.htaccess b/.htaccess index 6782611..c9db48e 100755 --- a/.htaccess +++ b/.htaccess @@ -7,7 +7,7 @@ Options -Indexes ## ADDED for - block any script execution inside folder of public - + Deny from all # Disable PHP engine diff --git a/app/Config/Acl.php b/app/Config/Acl.php new file mode 100644 index 0000000..1fbea05 --- /dev/null +++ b/app/Config/Acl.php @@ -0,0 +1,85 @@ + ['public' => true], + '#^/logout#' => ['public' => true], + '#^/auth#' => ['public' => true], + '#^/oauth2callback#' => ['public' => true], + '#^/loginPos#' => ['public' => true], + '#^/getVerifyPosMobileNo#' => ['public' => true], + '#^/getVerifiedPosUserData#' => ['public' => true], + '#^/swagger#' => ['roles' => [ADMIN_ROLE_ID]], + '#^/fedeploy#' => ['roles' => [ADMIN_ROLE_ID]], + + // ===================== PUBLIC DOWNLOADS / FORMS ===================== + '#^/download-#' => ['public' => true], + '#^/claim-form-download#' => ['public' => true], + '#^/claims-feedback-form#' => ['public' => true], + '#^/autobookstackLogin#' => ['public' => true], + + // ===================== DASHBOARD ===================== + '#^/dashboard#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [] + ], + + // ===================== USER MANAGEMENT ===================== + '#^/user#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID], + 'teams' => [] + ], + + // ===================== CLIENT ===================== + '#^/client#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [] + ], + + // ===================== EMPLOYEE / ENROLLMENT ===================== + '#^/employee#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [ENROLLMENT_TEAM_ID] + ], + + // ===================== MASTERS ===================== + '#^/master#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID], + 'teams' => [] + ], + '#^/util#' => [ + 'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID,STAFF_ROLE_ID], + 'teams' => [] + ], + + // ===================== LOGS ===================== + '#^/logs#' => [ + 'roles' => [ADMIN_ROLE_ID], + 'teams' => [] + ], + + // ===================== INTERNAL TEST ===================== + '#^/test#' => [ + 'roles' => [ADMIN_ROLE_ID], + 'teams' => [] + ], + + // ===================== API (JWT / SIGNED) ===================== + '#^/api#' => ['public' => true], + '#^/employeeRest#' => ['public' => true], + '#^/clientApi#' => ['public' => true], + // ===================== CLI ===================== + '#^/cli/#' => ['public' => true], + + // ===================== DEFAULT DENY (ZERO TRUST) ===================== + '#^/#' => [ + 'roles' => [ADMIN_ROLE_ID], + 'teams' => [] + ], + ]; +} diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 969dadf..e22e4af 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -18,6 +18,9 @@ use App\Filters\AuthJWT; use App\Filters\Cors; use App\Filters\GlobalPostFileUploadGuard; use App\Filters\SecurityInputFilter; +use App\Filters\AclFilter; + + class Filters extends BaseConfig { @@ -43,6 +46,7 @@ class Filters extends BaseConfig 'appSignature' => VerifyAppSignature::class, 'GlobalPostFileUploadGuard' => GlobalPostFileUploadGuard::class, 'SecurityInputFilter' => SecurityInputFilter::class, + 'AclFilter' => AclFilter::class, ]; @@ -56,6 +60,7 @@ class Filters extends BaseConfig public array $globals = [ 'before' => [ 'HttpRequestLog' => ['except' => 'cli/*'], + 'AclFilter' => ['except' => 'login', 'logout', 'auth/*', 'oauth2callback', 'download-*', 'claim-form-download', 'claims-feedback-form', 'autobookstackLogin'], 'Cors', 'SecurityInputFilter', 'GlobalPostFileUploadGuard', diff --git a/app/Controllers/LoginController.php b/app/Controllers/LoginController.php index eb3d98e..aa5d9a8 100755 --- a/app/Controllers/LoginController.php +++ b/app/Controllers/LoginController.php @@ -31,20 +31,22 @@ class LoginController extends BaseController public function receiveGoogleOAuthResponse() { $UserModel = new UserModel(); - //echo 'DONE';die(); + // echo 'DONE';die(); if ($this->request->getGet('code')) { $code = (string) $this->request->getGet('code'); log_message('error', 'Get OAuth Responce Code Sucessfully'); log_message('error', 'OAuthResponceCode : `'.$code.'`'); $value = googleOAuthLogin($this->request->getGet('code')); + // log_message('error', "OAuthResponceCode :" . json_encode($value)); if($value){ + // print_r($value);//die; $user = $UserModel->getUserByEmail($value->email); if($user){ if($user->is_active !== '0'){ $user_team = $UserModel->getUserTeamsByUserID($user->id); // dd($user_team); - + session()->regenerate(true); $session_data = [ 'isLoggedIn' => True , 'userid' => $user->id, @@ -66,7 +68,7 @@ class LoginController extends BaseController log_message('error', 'User Login Sucessfully'); $this->getUserDeviceInfo($user->id, 'NhanceUser'); - return AuthLogout::logout(); + return redirect()->to(base_url('/dashboard/view')); }else{ log_message('error', 'User Not Active'); diff --git a/app/Filters/AclFilter.php b/app/Filters/AclFilter.php new file mode 100644 index 0000000..0f6e0c1 --- /dev/null +++ b/app/Filters/AclFilter.php @@ -0,0 +1,156 @@ +getPath(), '/'); + + // Base path: /PHP828APPS/ruc/nhance + $basePath = rtrim(parse_url(base_url(), PHP_URL_PATH), '/'); + + // Remove base path + if ($basePath && str_starts_with($fullPath, $basePath)) { + $path = substr($fullPath, strlen($basePath)); + } else { + $path = $fullPath; + } + + // Remove index.php if present + if (str_starts_with($path, '/index.php')) { + $path = substr($path, strlen('/index.php')); + } + + // Normalize + $path = '/' . ltrim($path, '/'); + + // Fallback + if ($path === '') { + $path = '/'; + } + + // echo'
BASH PATH: ' . base_url(); + // echo'
ACL RAW PATH: ' . $fullPath; + // echo'
ACL BASE PATH: ' . $basePath; + // echo'
ACL FINAL PATH: ' . $path; + // ===================== LOAD ACL ===================== + $acl = new Acl(); + $rules = $acl->rules; + // print_rr($rules);die; + // ===================== MATCH RULE ===================== + $matchedRule = null; + + foreach ($rules as $pattern => $rule) { + // echo "$pattern".'---------
'; + if (preg_match($pattern, $path)) { + // echo "matched - $pattern"; + $matchedRule = $rule; + break; // FIRST MATCH WINS + } + } + + // print_r($matchedRule);//die; + // ===================== NO RULE = DENY ===================== + if ($matchedRule === null) { + return $this->deny($path, 'No ACL rule matched'); + } + + // ===================== PUBLIC ROUTE ===================== + if (!empty($matchedRule['public'])) { + return; // ALLOW + } + + // ===================== AUTH CHECK ===================== + if (!check_session()) { + // For API requests return 401 JSON + if ($request->isAJAX() || str_starts_with($path, '/api') || str_starts_with($path, '/employeeRest')) { + return service('response') + ->setStatusCode(401) + ->setJSON(['error' => 'Unauthorized']); + } + + // For web redirect to login + return AuthLogout::logout(); + } + + // ===================== GET USER CONTEXT ===================== + + $userRole = check_role(); // + $userTeams = user_team(); // must return array of TEAM IDs + + $allowedRoles = $matchedRule['roles'] ?? []; + $allowedTeams = $matchedRule['teams'] ?? []; + // ===================== ROLE FIRST ===================== + if (!empty($allowedRoles) && in_array((int)$userRole, $allowedRoles, true)) { + return; // ALLOW + } + // ===================== TEAM FALLBACK ===================== + if (!empty($allowedTeams) && is_array($userTeams)) { + foreach ($userTeams as $teamId) { + if (in_array($teamId, $allowedTeams, true)) { + return; // ALLOW + } + } + } + // ===================== DENY ===================== + return $this->deny($path, 'Role/Team not permitted'); + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + // nothing + } + + // ===================== DENY HANDLER ===================== + protected function deny(string $path, string $reason) + { + log_message('error', 'ACL BLOCKED: {user} {path} - {reason}', [ + 'user' => session()->get('userid') ?? 'guest', + 'path' => $path, + 'reason' => $reason, + ]); + + // API / AJAX → JSON + $request = service('request'); + if ($request->isAJAX() || str_starts_with($path, '/api') || str_starts_with($path, '/employeeRest')) { + return service('response') + ->setStatusCode(403) + ->setJSON([ + 'error' => 'Forbidden', + 'message' => 'You do not have permission to access this resource' + ]); + } + + $response = service('response'); + $response->setStatusCode(403); + $response->setBody(view('errors/404', [ + 'message' => '403 Access denied - You do not have permission to access this resource' + ])); + + return $response; + // Web → nice 403 page or simple text + return service('response') + ->setStatusCode(403) + ->setBody('403 Forbidden - Access denied - You do not have permission to access this resource'); + } +} diff --git a/app/Libraries/AuthLogout.php b/app/Libraries/AuthLogout.php index cfb1005..ac7786c 100644 --- a/app/Libraries/AuthLogout.php +++ b/app/Libraries/AuthLogout.php @@ -22,7 +22,7 @@ class AuthLogout setcookie( session_name(), // DO NOT hardcode cookie name - '', + null, time() - 42000, $params['path'], $params['domain'], From d59aeae917352b69ddfbc187b3f1524122ac868f Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 12 Jan 2026 11:57:02 +0530 Subject: [PATCH 06/15] gwm : wrong mpin --- app/Config/Routes.php | 2 +- app/Controllers/RestAuthenticationController.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 2610b9e..4c02e4f 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -453,7 +453,7 @@ $routes->group("/api", ["filter" => "authJWT"], function ($routes) { // $routes->post("employeeRest/createOrUpdateEmployeePolicySiAmount", "EmployeeRestController::createOrUpdateEmployeePolicySiAmount"); // $routes->post("employeeRest/calculatePremium", "EmployeeRestController::calculatePremium"); - + // $routes->post("updateMpin", "RestAuthenticationController::updateMpin"); $routes->group("employeeRest", ['filter' => ['appSignature' , 'authJWT'] ], function ($routes) { $routes->post('logout', 'RestAuthenticationController::logout'); diff --git a/app/Controllers/RestAuthenticationController.php b/app/Controllers/RestAuthenticationController.php index 15469ec..41a716d 100755 --- a/app/Controllers/RestAuthenticationController.php +++ b/app/Controllers/RestAuthenticationController.php @@ -986,7 +986,7 @@ class RestAuthenticationController extends AdminController log_message('error', ' '); log_message('error', '************************ PRE END ********************************'); - $result = ['user_verification' => false , 'message' => "User not found"]; + $result = ['mpin_verification' => false , 'message' => "Wrong Mpin"]; return $this->respond(['status' => 'failed','code' => 404,'data' => $result],200); } From a5628280c680a43d2f4edbf6d0b3d5d00eb6a46e Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 12 Jan 2026 12:04:44 +0530 Subject: [PATCH 07/15] gwm : wrong mpin --- app/Config/Filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Config/Filters.php b/app/Config/Filters.php index e22e4af..78da473 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -46,7 +46,7 @@ class Filters extends BaseConfig 'appSignature' => VerifyAppSignature::class, 'GlobalPostFileUploadGuard' => GlobalPostFileUploadGuard::class, 'SecurityInputFilter' => SecurityInputFilter::class, - 'AclFilter' => AclFilter::class, + // 'AclFilter' => AclFilter::class, ]; From 51689bd82caa08d94b4e5d4ff002c67aeb2db28a Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 12 Jan 2026 12:07:51 +0530 Subject: [PATCH 08/15] gwm : wrong mpin --- app/Config/Filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 78da473..384d143 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -60,7 +60,7 @@ class Filters extends BaseConfig public array $globals = [ 'before' => [ 'HttpRequestLog' => ['except' => 'cli/*'], - 'AclFilter' => ['except' => 'login', 'logout', 'auth/*', 'oauth2callback', 'download-*', 'claim-form-download', 'claims-feedback-form', 'autobookstackLogin'], + // 'AclFilter' => ['except' => 'login', 'logout', 'auth/*', 'oauth2callback', 'download-*', 'claim-form-download', 'claims-feedback-form', 'autobookstackLogin'], 'Cors', 'SecurityInputFilter', 'GlobalPostFileUploadGuard', From 80e47ce1c9ed6c1c7d4fe9e590492040c5804a12 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 12 Jan 2026 12:37:48 +0530 Subject: [PATCH 09/15] gwm : mpin message changed --- app/Controllers/RestAuthenticationController.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Controllers/RestAuthenticationController.php b/app/Controllers/RestAuthenticationController.php index 41a716d..235f86a 100755 --- a/app/Controllers/RestAuthenticationController.php +++ b/app/Controllers/RestAuthenticationController.php @@ -978,6 +978,12 @@ class RestAuthenticationController extends AdminController $old_mpin = $this->request->getJSON()->old_mpin; $mpin = $this->request->getJSON()->new_mpin; + if($old_mpin == $mpin) + { + $result = ['mpin_verification' => false , 'message' => "New MPIN must be different from the old MPIN"]; + return $this->respond(['status' => 'failed','code' => 404,'data' => $result],200); + } + $empdata = RestAuthHelper::getPreAndPostDataByEmailOrMobile(['email_id' => $email_id, 'mobile_number' => $mobile_number, 'old_mpin' => $old_mpin ]); // print_r($empdata); die; @@ -986,7 +992,7 @@ class RestAuthenticationController extends AdminController log_message('error', ' '); log_message('error', '************************ PRE END ********************************'); - $result = ['mpin_verification' => false , 'message' => "Wrong Mpin"]; + $result = ['mpin_verification' => false , 'message' => "Old MPIN is incorrect"]; return $this->respond(['status' => 'failed','code' => 404,'data' => $result],200); } From ffc6c7f742c730d349a9655b2b3c8772caf6eba9 Mon Sep 17 00:00:00 2001 From: velz Date: Mon, 12 Jan 2026 14:28:13 +0530 Subject: [PATCH 10/15] FIX_ACL&HTTP_REQ_LOG --- app/Config/Filters.php | 2 +- app/Helpers/HttpRequestHelper.php | 177 +++++++++++++++++++++++++++--- 2 files changed, 165 insertions(+), 14 deletions(-) diff --git a/app/Config/Filters.php b/app/Config/Filters.php index e22e4af..08752c5 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -60,7 +60,7 @@ class Filters extends BaseConfig public array $globals = [ 'before' => [ 'HttpRequestLog' => ['except' => 'cli/*'], - 'AclFilter' => ['except' => 'login', 'logout', 'auth/*', 'oauth2callback', 'download-*', 'claim-form-download', 'claims-feedback-form', 'autobookstackLogin'], + 'AclFilter' => ['except' => ['login', 'logout', 'auth/*', 'oauth2callback','claim-form-download', 'claims-feedback-form', 'autobookstackLogin','/employeeRest/*']], 'Cors', 'SecurityInputFilter', 'GlobalPostFileUploadGuard', diff --git a/app/Helpers/HttpRequestHelper.php b/app/Helpers/HttpRequestHelper.php index a3a3da6..3e072e8 100755 --- a/app/Helpers/HttpRequestHelper.php +++ b/app/Helpers/HttpRequestHelper.php @@ -4,24 +4,175 @@ namespace App\Helpers; class HttpRequestHelper { - public static function getRequestInfo() + public static function getRequestInfo(): array { $request = service('request'); + + $uaString = $request->getHeaderLine('User-Agent'); + + // Detect platform & browser using robust fallback logic + [$platform, $browser] = self::detectFromUserAgent($uaString); + $data = [ - 'ip' => $request->getIPAddress(), - 'platform' => $request->getUserAgent()->getPlatform(), - 'browser' => ($request->getUserAgent()->getBrowser().' '.$request->getUserAgent()->getVersion()), - 'method' => $request->getMethod(), - 'endpoint' => $request->uri->getPath(), - 'getparams' => $request->uri->getSegments(), - 'postparams' => $request->getPost() + 'ip' => $request->getIPAddress(), + 'platform' => $platform, + 'browser' => $browser, + 'method' => strtoupper($request->getMethod()), + 'endpoint' => $request->uri->getPath(), + 'getparams' => json_encode($request->uri->getSegments(), JSON_UNESCAPED_UNICODE), + 'postparams' => self::sanitizePostForLog($request->getPost()), ]; - $data['method'] = (isset($data['method']) ? strtoupper($data['method']) : $data['method']); - $data['getparams'] = is_array($data['getparams']) ? json_encode($data['getparams']) : $data['getparams']; - $data['postparams'] = is_array($data['postparams']) ? json_encode($data['postparams']) : $data['postparams']; return $data; } - public static function add($payload) - {return $payload['a'] + $payload['b'];} + + /** + * Detect platform & browser from UA string (reliable fallback) + */ + private static function detectFromUserAgent(string $ua): array + { + $uaLower = strtolower($ua); + + // ========================= + // PLATFORM DETECTION + // ========================= + $platform = 'Unknown'; + + if (str_contains($uaLower, 'windows nt 11') || str_contains($uaLower, 'windows 11')) { + $platform = 'Windows 11'; + } elseif (str_contains($uaLower, 'windows nt 10')) { + $platform = 'Windows 10'; + } elseif (str_contains($uaLower, 'windows nt 6.3')) { + $platform = 'Windows 8.1'; + } elseif (str_contains($uaLower, 'windows nt 6.2')) { + $platform = 'Windows 8'; + } elseif (str_contains($uaLower, 'windows nt 6.1')) { + $platform = 'Windows 7'; + } elseif (str_contains($uaLower, 'windows nt 6.0')) { + $platform = 'Windows Vista'; + } elseif (str_contains($uaLower, 'windows nt 5.1') || str_contains($uaLower, 'windows xp')) { + $platform = 'Windows XP'; + } elseif (str_contains($uaLower, 'android')) { + $platform = 'Android'; + } elseif (str_contains($uaLower, 'iphone')) { + $platform = 'iOS (iPhone)'; + } elseif (str_contains($uaLower, 'ipad')) { + $platform = 'iOS (iPad)'; + } elseif (str_contains($uaLower, 'ipod')) { + $platform = 'iOS (iPod)'; + } elseif (str_contains($uaLower, 'mac os') || str_contains($uaLower, 'macintosh')) { + $platform = 'Mac OS'; + } elseif (str_contains($uaLower, 'cros')) { + $platform = 'Chrome OS'; + } elseif (str_contains($uaLower, 'linux')) { + $platform = 'Linux'; + } elseif (str_contains($uaLower, 'freebsd')) { + $platform = 'FreeBSD'; + } elseif (str_contains($uaLower, 'openbsd')) { + $platform = 'OpenBSD'; + } elseif (str_contains($uaLower, 'netbsd')) { + $platform = 'NetBSD'; + } elseif (str_contains($uaLower, 'unix')) { + $platform = 'Unix'; + } elseif (str_contains($uaLower, 'symbian')) { + $platform = 'Symbian'; + } elseif (str_contains($uaLower, 'blackberry')) { + $platform = 'BlackBerry'; + } elseif (str_contains($uaLower, 'tizen')) { + $platform = 'Tizen'; + } elseif (str_contains($uaLower, 'webos')) { + $platform = 'WebOS'; + } elseif (str_contains($uaLower, 'kaios')) { + $platform = 'KaiOS'; + } elseif (str_contains($uaLower, 'harmonyos')) { + $platform = 'HarmonyOS'; + } elseif (str_contains($uaLower, 'watchos')) { + $platform = 'watchOS'; + } elseif (str_contains($uaLower, 'tv os') || str_contains($uaLower, 'tvos')) { + $platform = 'tvOS'; + } + + // ========================= + // BROWSER / CLIENT DETECTION + // ========================= + $browser = 'Unknown'; + + // Bots & tools first + if (preg_match('/googlebot|bingbot|slurp|duckduckbot|baiduspider|yandexbot|sogou|exabot|facebot|ia_archiver/i', $ua)) { + $browser = 'Search Bot'; + } elseif (preg_match('/postman/i', $ua)) { + $browser = 'Postman'; + } elseif (preg_match('/insomnia/i', $ua)) { + $browser = 'Insomnia'; + } elseif (preg_match('/curl/i', $ua)) { + $browser = 'curl'; + } elseif (preg_match('/wget/i', $ua)) { + $browser = 'wget'; + } + + // Real browsers + elseif (preg_match('/edg\/([\d\.]+)/i', $ua, $m)) { + $browser = 'Edge ' . $m[1]; + } elseif (preg_match('/opr\/([\d\.]+)/i', $ua, $m)) { + $browser = 'Opera ' . $m[1]; + } elseif (preg_match('/vivaldi\/([\d\.]+)/i', $ua, $m)) { + $browser = 'Vivaldi ' . $m[1]; + } elseif (preg_match('/brave\/([\d\.]+)/i', $ua, $m)) { + $browser = 'Brave ' . $m[1]; + } elseif (preg_match('/chrome\/([\d\.]+)/i', $ua, $m)) { + $browser = 'Chrome ' . $m[1]; + } elseif (preg_match('/firefox\/([\d\.]+)/i', $ua, $m)) { + $browser = 'Firefox ' . $m[1]; + } elseif (preg_match('/safari\/([\d\.]+)/i', $ua, $m)) { + $browser = 'Safari ' . $m[1]; + } elseif (preg_match('/msie\s([\d\.]+)/i', $ua, $m) || preg_match('/trident\/.*rv:([\d\.]+)/i', $ua, $m)) { + $browser = 'Internet Explorer ' . $m[1]; + } + + // In-app browsers + elseif (preg_match('/fbav|fban/i', $ua)) { + $browser = 'Facebook In-App Browser'; + } elseif (preg_match('/instagram/i', $ua)) { + $browser = 'Instagram In-App Browser'; + } elseif (preg_match('/linkedinapp/i', $ua)) { + $browser = 'LinkedIn In-App Browser'; + } elseif (preg_match('/twitter/i', $ua)) { + $browser = 'Twitter/X In-App Browser'; + } + + return [$platform, $browser]; + } + + + /** + * Remove sensitive fields before logging POST + */ + private static function sanitizePostForLog(array $post): string + { + if (empty($post)) { + return json_encode([]); + } + + $sensitiveKeys = [ + 'password', 'pass', 'pwd', + 'token', 'access_token', 'refresh_token', + 'secret', 'api_key', 'authorization', + 'otp', 'pin' + ]; + + foreach ($post as $k => $v) { + foreach ($sensitiveKeys as $sk) { + if (stripos($k, $sk) !== false) { + $post[$k] = '***MASKED***'; + } + } + } + + return json_encode($post, JSON_UNESCAPED_UNICODE); + } + + public static function add($payload) + { + return $payload['a'] + $payload['b']; + } } From b39eac1f31816ccfedf441c9a5f36d717d583a6b Mon Sep 17 00:00:00 2001 From: velz Date: Tue, 13 Jan 2026 10:36:42 +0530 Subject: [PATCH 11/15] FIX_mPIN_NULl_HANDLED --- app/Config/Filters.php | 2 +- app/Controllers/RestAuthenticationController.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 08752c5..e14b7d8 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -60,7 +60,7 @@ class Filters extends BaseConfig public array $globals = [ 'before' => [ 'HttpRequestLog' => ['except' => 'cli/*'], - 'AclFilter' => ['except' => ['login', 'logout', 'auth/*', 'oauth2callback','claim-form-download', 'claims-feedback-form', 'autobookstackLogin','/employeeRest/*']], + 'AclFilter' => ['except' => ['login', 'logout', 'auth/*', 'oauth2callback','claim-form-download', 'claims-feedback-form', 'autobookstackLogin','/employeeRest/*','processjob']], 'Cors', 'SecurityInputFilter', 'GlobalPostFileUploadGuard', diff --git a/app/Controllers/RestAuthenticationController.php b/app/Controllers/RestAuthenticationController.php index 235f86a..af58a6d 100755 --- a/app/Controllers/RestAuthenticationController.php +++ b/app/Controllers/RestAuthenticationController.php @@ -1271,7 +1271,7 @@ class RestAuthenticationController extends AdminController $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - checkMpin: Mpin - Exist"); log_message('error', ' '); log_message('error', '************************ PRE END ********************************'); - return $this->respond(['status' => 'success','code' => 200,'data' => "", 'message' => "Mpin - exist" , 'Mpin' =>$employeeData["mpin"], 'is_mpin_skipped' => $employeeData['is_mpin_skipped'], 'is_biometric_enabled' => $employeeData['is_biometric_enabled']],200); + return $this->respond(['status' => 'success','code' => 200,'data' => "", 'message' => "Mpin - exist" , 'Mpin' =>$employeeData["mpin"], 'is_mpin_skipped' => $employeeData['is_mpin_skipped'] ?? 0, 'is_biometric_enabled' => $employeeData['is_biometric_enabled'] ?? 0 ],200); } else { $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - checkMpin: Mpin - not found in PRE so call the thirdpartapi to the POST to check the MPIN"); log_message('error', ' '); From 59dd0644b684864f2e7a8e13b021de6d90303295 Mon Sep 17 00:00:00 2001 From: velz Date: Tue, 13 Jan 2026 11:08:34 +0530 Subject: [PATCH 12/15] FIX_EMAIL_ROUTE_EXCEPT_IN_I/P_SANITIZATION --- app/Config/Filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Config/Filters.php b/app/Config/Filters.php index e14b7d8..ecf5672 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -62,7 +62,7 @@ class Filters extends BaseConfig 'HttpRequestLog' => ['except' => 'cli/*'], 'AclFilter' => ['except' => ['login', 'logout', 'auth/*', 'oauth2callback','claim-form-download', 'claims-feedback-form', 'autobookstackLogin','/employeeRest/*','processjob']], 'Cors', - 'SecurityInputFilter', + 'SecurityInputFilter' => ['except' => ['notification/create','test_mail'] ], 'GlobalPostFileUploadGuard', // 'invalidchars', ], From e42b3fcc5f9844035d6b19d4e4c7ae25b99a6a9a Mon Sep 17 00:00:00 2001 From: velz Date: Tue, 13 Jan 2026 16:52:29 +0530 Subject: [PATCH 13/15] FIX_LIVE_AGGREE_API_ISSUE --- app/Controllers/EmployeeRestController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index e810120..e4e9577 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -2709,7 +2709,7 @@ class EmployeeRestController extends AdminController foreach ($client_policy_id as $key => $value) { // $policy = $this->clientPolicyModel->where('id',$value)->where('open_for_enrollment',1)->find(); - $policy = $this->findThePolicyIsOpenForEnrollment($client_policy_id, $emp_code); + $policy = $this->findThePolicyIsOpenForEnrollment($value, $emp_code); if($policy) { $this->myLogger->logme("error", 'client policy id = '.$value.' is open for enrollment'); From e5592464915bfd9a1cf41d6a73569c662505d8d4 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Tue, 13 Jan 2026 18:14:13 +0530 Subject: [PATCH 14/15] gwm : open for enrollment check --- app/Controllers/EmployeeRestController.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index e4e9577..b2f844d 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -2320,6 +2320,13 @@ class EmployeeRestController extends AdminController } + if(isset($array['base_policy']) && !empty($array['base_policy'])) + { + $openForEnrollmentValue = (int) $this->findThePolicyIsOpenForEnrollment($array['base_policy'], $emp_code); + }else{ + $openForEnrollmentValue = $array['open_for_enrollment']; + } + $policyTypeData = $this->policyTypeModel->where('id',$array['policy_type_id'])->get()->getRow(); $responce['policy_name'] = $policyTypeData->long_name; @@ -2329,7 +2336,7 @@ class EmployeeRestController extends AdminController $responce['client_id'] = $array['client_id']; $responce['client_policy_id'] = $array['id']; $responce['is_addon'] = $array['is_addon']; - $responce['OpenForEnrollment'] = $array['open_for_enrollment']; + $responce['OpenForEnrollment'] = $openForEnrollmentValue; $responce['policy_terms'] = $decodedArray; $responce['policy_type_id'] = $array['policy_type_id']; //$responce['is_member_modify_allowed'] = $array['is_member_modify_allowed']; From f4c4551dd892dffcb1c3df98c322c75a4ccad672 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Tue, 13 Jan 2026 18:49:33 +0530 Subject: [PATCH 15/15] GWM : iAgreeForAddOn policy handled --- app/Controllers/EmployeeRestController.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index b2f844d..2d5a2a7 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -2715,8 +2715,12 @@ class EmployeeRestController extends AdminController $array_list = []; foreach ($client_policy_id as $key => $value) { - // $policy = $this->clientPolicyModel->where('id',$value)->where('open_for_enrollment',1)->find(); - $policy = $this->findThePolicyIsOpenForEnrollment($value, $emp_code); + + $clientPolicyData = $this->clientPolicyModel->where('client_id',$client_id) + ->where('id',$value) + ->where('is_active', 1) + ->first(); + $policy = $this->findThePolicyIsOpenForEnrollment($clientPolicyData['base_policy'], $emp_code); if($policy) { $this->myLogger->logme("error", 'client policy id = '.$value.' is open for enrollment');