diff --git a/app/Config/Filters.php b/app/Config/Filters.php index c9431b94..6c031ae7 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -20,6 +20,8 @@ use App\Filters\SecurityInputFilter; use App\Filters\GlobalPostFileUploadGuard; use App\Filters\AclFilter; use App\Filters\RateLimitFilter; +use App\Filters\JwtApiRateLimitFilter; +use App\Filters\AuthApiRateLimitFilter; use App\Filters\AuthJWT; @@ -51,6 +53,8 @@ class Filters extends BaseConfig 'GlobalPostFileUploadGuard' => GlobalPostFileUploadGuard::class, 'AclFilter' => AclFilter::class, 'ratelimit' => RateLimitFilter::class, + 'AuthApiRateLimitFilter' => AuthApiRateLimitFilter::class, + 'JwtApiRateLimitFilter' => JwtApiRateLimitFilter::class, ]; diff --git a/app/Config/RateLimiter.php b/app/Config/RateLimiter.php new file mode 100644 index 00000000..99d8b60a --- /dev/null +++ b/app/Config/RateLimiter.php @@ -0,0 +1,89 @@ + 60, // max requests + 'window' => 60, // window in seconds + 'violation_soft' => 3, // violations before soft block + ]; + + /* + |-------------------------------------------------------------------------- + | Auth API Routes (verifyMobile, verifyOTP, etc.) + |-------------------------------------------------------------------------- + */ + public array $authApi = [ + 'limit' => 10, // max requests per window + 'window' => 180, // window in seconds (3 min) + 'violation_soft' => 3, // failed attempts before soft block + ]; + + /* + |-------------------------------------------------------------------------- + | User-Level Progressive Block Durations (seconds) + | 0 = permanent until manual unblock + |-------------------------------------------------------------------------- + */ + public array $userBlock = [ + 'soft_duration' => 0, // permanent, manual unblock only + 'medium_duration' => 7200, // 2 hours + 'hard_duration' => 86400, // 24 hours + // attempts while at a block level before escalating to next + 'medium_trigger' => 1, // attempts during soft → medium + 'hard_trigger' => 1, // attempts during medium → hard + ]; + + /* + |-------------------------------------------------------------------------- + | IP-Level Throttle & Progressive Block (independent of user) + |-------------------------------------------------------------------------- + */ + public array $ipBlock = [ + 'limit' => 120, // max requests per window + 'window' => 60, // window in seconds + 'violation_soft' => 5, // violations before soft block + 'soft_duration' => 0, // permanent, manual unblock only + 'medium_duration' => 7200, // 2 hours + 'hard_duration' => 86400, // 24 hours + 'medium_trigger' => 1, // attempts during soft → medium + 'hard_trigger' => 1, // attempts during medium → hard + ]; + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefixes + |-------------------------------------------------------------------------- + */ + public array $cacheKeys = [ + 'ip_count' => 'rl_ip_count_', + 'ip_violations' => 'rl_ip_viol_', + 'ip_block' => 'rl_ip_block_', + 'ip_block_hits' => 'rl_ip_blkhit_', + 'user_count' => 'rl_usr_count_', + 'user_violations' => 'rl_usr_viol_', + 'user_block' => 'rl_usr_block_', + 'user_block_hits' => 'rl_usr_blkhit_', + ]; + + /* + |-------------------------------------------------------------------------- + | HTTP Status Codes per block level + |-------------------------------------------------------------------------- + */ + public array $statusCodes = [ + 'throttle' => 429, + 'soft' => 429, + 'medium' => 403, + 'hard' => 451, + ]; +} diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 43d45fac..efde3147 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -10,6 +10,15 @@ $routes->options('(:any)', function() { // But having this route ensures OPTIONS isn't rejected as 404 }); +$routes->group('sheet', function ($routes) { + $routes->get('(:any)', 'GoogleSheetController::editor/$1'); + $routes->get('(:any)/fetch', 'GoogleSheetController::fetch/$1'); + $routes->post('(:any)/save', 'GoogleSheetController::save/$1'); + $routes->get('(:any)/download', 'GoogleSheetController::download/$1'); +}); + + + /** * @var RouteCollection $routes */ diff --git a/app/Controllers/EmployeeController.php b/app/Controllers/EmployeeController.php index fae64f2d..44afb184 100755 --- a/app/Controllers/EmployeeController.php +++ b/app/Controllers/EmployeeController.php @@ -3669,7 +3669,7 @@ class EmployeeController extends AdminController "policyStartDate" => $policyStartDate, "policyEndDate" => $policyEndDate, "plan" => $primary["wellness_plan_id"] ?? null, - "source" => $primary['short_name'] ?? null, + "source" => 'NHANCE', "employer" => $primary['short_name'] ?? null, "employeeCode" => $empCode, "accountNumber" => "", // Fill from DB if available @@ -3690,8 +3690,8 @@ class EmployeeController extends AdminController "name" => $row["name"], "phone" => $row["mobile"], "email" => $row["email_corporate"], - "relationshipName" => strtoupper($row["relationship"] ?? ''), - "gender" => $row["gender"], + "relationshipName" => $this->mapRelationship($row["relationship"],$row["gender"]), + "gender" => $row["gender"] == 'M' ? 'Male' : 'Female', "dob" => $row["dob"] ]; } @@ -3700,6 +3700,42 @@ class EmployeeController extends AdminController } + function mapRelationship(string $relationship, ?string $gender = null): string + { + // Normalize input + $key = strtolower(trim($relationship)); + $key = str_replace(['-', '_'], ' ', $key); + $key = preg_replace('/\s+/', ' ', $key); + + // Base mapping + $map = [ + 'self' => 'self', + 'son' => 'son', + 'daughter' => 'daughter', + 'father' => 'father', + 'mother' => 'mother', + 'father in law' => 'father in law', + 'father-in-law' => 'father in law', + 'mother in law' => 'mother in law', + 'mother-in-law' => 'mother in law', + ]; + + // Special handling for spouse + if ($key === 'spouse') { + if ($gender === 'M') { + return 'husband'; + } + if ($gender === 'F') { + return 'wife'; + } + + // fallback if gender missing (better to throw error) + throw new Exception("Gender required to map 'Spouse'"); + } + + return $map[$key] ?? ''; + } + /** * Send each family payload to API and attach the response diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index 1d1763d3..bf64515c 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -3769,7 +3769,11 @@ class EmployeeRestController extends AdminController $fetchData['priority'] = 1; $fetchData['mode_of_intimation'] = 3; - $fetchData['claim_type'] = 1; + + if(!isset($received_data['claim_type']) || (isset($received_data['claim_type']) && empty($received_data['claim_type']))) { + $received_data['claim_type'] = 1; + } + $fetchData = array_merge($fetchData, $received_data); // $fetchData['relationship'] = strtolower($fetchData['relationship']) ?? $fetchData['relationship']; @@ -3993,8 +3997,9 @@ class EmployeeRestController extends AdminController // ])->setStatusCode(400); $ticket_type = $this->ticketController->ticketType; + $claim_type = $this->ticketController->claimType; $ticket_type = array_map(fn($value, $key) => (object) ['id' => $key, 'name' => $value], array_values($ticket_type), array_keys($ticket_type)); - return $this->response->setJSON(['ticket_type' => $ticket_type])->setStatusCode(200); + return $this->response->setJSON(['ticket_type' => $ticket_type, 'claim_type' => $claim_type])->setStatusCode(200); } // Get policy type ids @@ -4023,6 +4028,8 @@ class EmployeeRestController extends AdminController $this->myLogger->logme('error', "Policy type IDs fetched: " . json_encode($policy_type_ids)); $ticket_type = $this->ticketController->ticketType; + $claim_type = $this->ticketController->claimType; + $filtered = []; $mapping = [ @@ -4056,7 +4063,7 @@ class EmployeeRestController extends AdminController } $filtered = array_values($filtered); - return $this->response->setJSON(['status' => true, 'code' => 200, 'ticket_type' => $filtered])->setStatusCode(200); + return $this->response->setJSON(['status' => true, 'code' => 200, 'ticket_type' => $filtered, 'claim_type' => $claim_type])->setStatusCode(200); } catch (\Throwable $e) { $this->myLogger->logme('error', "Error in get_ticket_type: " . $e->getMessage() . " Trace: " . $e->getTraceAsString()); return $this->response->setJSON([ diff --git a/app/Controllers/GoogleSheetController.php b/app/Controllers/GoogleSheetController.php new file mode 100644 index 00000000..e51f81ab --- /dev/null +++ b/app/Controllers/GoogleSheetController.php @@ -0,0 +1,66 @@ +sheetLib = new GoogleSheetLib(); + } + + /* ---------- UI ---------- */ + public function editor(string $sheetId) + { + return view('gsheet_editor', [ + 'sheetId' => $sheetId + ]); + } + + /* ---------- FETCH ---------- */ + public function fetch(string $sheetId) + { + // $data = $this->sheetLib->read($sheetId); + // print_rr($data);die; + // return $this->response->setJSON($data); + echo 'Hi'; + } + + /* ---------- SAVE ---------- */ + public function save(string $sheetId) + { + $rows = $this->request->getJSON(true); + + if (!is_array($rows)) { + return $this->response + ->setStatusCode(400) + ->setJSON(['error' => 'Invalid data']); + } + + $this->sheetLib->write($sheetId, $rows); + + return $this->response->setJSON([ + 'status' => 'success' + ]); + } + + /* ---------- DOWNLOAD ---------- */ + public function download(string $sheetId) + { + $content = $this->sheetLib->downloadExcel($sheetId); + + return $this->response + ->setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + ->setHeader( + 'Content-Disposition', + 'attachment; filename="sheet.xlsx"' + ) + ->setBody($content); + } +} diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index b826c277..b4eaac06 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -4319,27 +4319,27 @@ class LeadsController extends BaseController } } - public function getLastFiveFinancialYears() + public function getLastFiveFinancialYears(): array { - $currentYear = date('Y'); - $currentMonth = date('m'); + $year = (int) date('Y'); + $month = (int) date('m'); + // $month = (int) 5; - // In India, the financial year starts from April (04) - if ($currentMonth < 4) { - $currentYear--; // Adjust year if it's Jan-Mar - } + // Financial year starts in April + $currentFYStart = ($month >= 4) ? $year : $year - 1; $financialYears = []; - for ($i = 0; $i < 5; $i++) { - $startYear = $currentYear - $i - 1; - $endYear = $currentYear - $i; - $financialYears[] = "$startYear-$endYear"; + for ($i = 0; $i < 6; $i++) { + $startYear = $currentFYStart - $i; + $endYear = $startYear + 1; + $financialYears[] = "{$startYear}-{$endYear}"; } return $financialYears; } + // ------------ RFQ NON EB FUNCTIONS----------------------------------------------------------------------------------------- public function rfqNonEB() diff --git a/app/Controllers/TestingController.php b/app/Controllers/TestingController.php index 7cfc5ab2..66957ea8 100644 --- a/app/Controllers/TestingController.php +++ b/app/Controllers/TestingController.php @@ -990,16 +990,24 @@ class TestingController extends BaseController // 🔐 Move this to .env in real projects $METABASE_SECRET_KEY = getenv('METABASE_SECRET_KEY'); - $policy_id = $this->request->getGet('client_policy'); + $database_id = (int)$this->request->getGet('database_id') ?? 2; + $policy_id = $this->request->getGet('client_policy') ?? null; + $tpa_url = 'https://nsights.nhanceindia.in/public/dashboard/4babf324-6c1e-4c5a-adbb-1c80a0f545b1'; $policy_id = $policy_id ? $policy_id : 4687; $payload = [ 'resource' => [ // 'dashboard' => 1 - 'dashboard' => 2 + 'dashboard' => $database_id ], - 'params' => (object)['client_policy' => $policy_id], // MUST be object for Metabase 'exp' => time() + (10 * 60) // 10 minutes ]; + + if(!empty($policy_id)){ + $payload['params'] = (object)['client_policy' => $policy_id]; // MUST be object for Metabase + }else{ + $payload['params'] = (object)[]; // MUST be object for Metabase + } + // dd($payload); $token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256'); @@ -1025,6 +1033,31 @@ class TestingController extends BaseController ]); } + public function testingquerys1(){ + } + public function testingquerys() + { + $calendar = new \App\Libraries\GoogleCalendarService(); + + // Check if user is authenticated without passing tokens manually + if (!$calendar->isReady()) { + return $this->respond(['status' => 'failed', 'code' => '404' , 'message' => 'Google Access Token Expired'], 200); + } + + $data = [ + 'summary' => 'Client Follow up', + 'meeting_date' => '2026-02-22 10:00:00', + 'description' => 'Visit Client place', + 'emails' => ['surendarsuri30@gmail.com', 'vitvelz@gmail.com', 'venbalap2026@gmail.com', 'gowthamceline46@gmail.com'] + ]; + + try { + $response = $calendar->createEvent($data); + return $this->respond(['status' => 'success', 'code' => '200' , 'message' => 'Follow-up Saved', 'response' => $response], 200); + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => '500' , 'message' => 'Error: ' . $e->getMessage()], 200); + } + } } diff --git a/app/Filters/AuthApiRateLimitFilter.php b/app/Filters/AuthApiRateLimitFilter.php new file mode 100644 index 00000000..3dc0db21 --- /dev/null +++ b/app/Filters/AuthApiRateLimitFilter.php @@ -0,0 +1,141 @@ +post('api/auth/verify-otp', 'AuthController::verifyOtp', ['filter' => 'authApiRateLimit']); + * + * Register in app/Config/Filters.php: + * 'AuthApiRateLimitFilter' => \App\Filters\AuthApiRateLimitFilter::class + */ +class AuthApiRateLimitFilter implements FilterInterface +{ + protected RateLimiterService $limiter; + + public function __construct() + { + $this->limiter = new RateLimiterService(); + } + + // ------------------------------------------------------------------------- + // BEFORE — runs before the controller + // ------------------------------------------------------------------------- + + public function before(RequestInterface $request, $arguments = null) + { + $fingerprint = generateFingerprint(); + + // 1. IP-level check + $ipResult = $this->limiter->checkIp($fingerprint, 'authApi'); + if ($ipResult) { + return $this->jsonResponse($ipResult); + } + + // 2. User-level block check (identity may not be present yet on first hit) + $identity = $this->resolveIdentity($request); + if ($identity) { + $userResult = $this->limiter->checkUser($identity); + if ($userResult) { + return $this->jsonResponse($userResult); + } + } + + // Store resolved identity in request for use in after() + if ($identity) { + $request->setGlobal('rateLimitIdentity', $identity); + } + $request->setGlobal('rateLimitFingerprint', $fingerprint); + + return null; // pass through + } + + // ------------------------------------------------------------------------- + // AFTER — runs after the controller; records failures on bad responses + // ------------------------------------------------------------------------- + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + // Only act on failed responses (4xx from auth failures) + $statusCode = $response->getStatusCode(); + 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); + + // Record failure at IP level + $this->limiter->recordIpFailure($fingerprint); + + // Record failure at user level + if ($identity) { + $this->limiter->recordUserFailure($identity, 'authApi'); + } + } + + // ------------------------------------------------------------------------- + // 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. + */ + protected function jsonResponse(array $result): ResponseInterface + { + $response = service('response'); + $response->setStatusCode($result['status']); + $response->setContentType('application/json'); + $response->setBody(json_encode([ + 'success' => false, + 'error' => [ + 'code' => strtoupper('RATE_LIMIT_' . $result['level']), + 'message' => $result['message'], + 'type' => $result['type'] ?? 'request', + ], + ])); + return $response; + } +} diff --git a/app/Filters/JwtApiFilter.php b/app/Filters/JwtApiFilter.php new file mode 100644 index 00000000..2c887b68 --- /dev/null +++ b/app/Filters/JwtApiFilter.php @@ -0,0 +1,151 @@ +get('api/profile', 'ProfileController::index', ['filter' => 'jwtApiRateLimit']); + * + * Register in app/Config/Filters.php: + * 'JwtApiRateLimitFilter' => \App\Filters\JwtApiRateLimitFilter::class + */ +class JwtApiRateLimitFilter implements FilterInterface +{ + protected RateLimiterService $limiter; + + public function __construct() + { + $this->limiter = new RateLimiterService(); + } + + // ------------------------------------------------------------------------- + // BEFORE — runs before the controller + // ------------------------------------------------------------------------- + + public function before(RequestInterface $request, $arguments = null) + { + $fingerprint = generateFingerprint(); + + // 1. IP-level throttle + block check + $ipResult = $this->limiter->checkIp($fingerprint, 'jwtApi'); + if ($ipResult) { + return $this->jsonResponse($ipResult); + } + + // 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(); + + if ($identity) { + // User-level throttle (request count based for JWT routes) + $userResult = $this->limiter->checkUserThrottle($identity, 'jwtApi'); + if ($userResult) { + return $this->jsonResponse($userResult); + } + } + + // Stash for after() use + $request->setGlobal('rateLimitFingerprint', $fingerprint); + if ($identity) { + $request->setGlobal('rateLimitIdentity', $identity); + } + + return null; // pass through + } + + // ------------------------------------------------------------------------- + // AFTER — records IP failure on controller-level bad responses + // ------------------------------------------------------------------------- + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + $statusCode = $response->getStatusCode(); + + // Only act on auth-related failures from the controller (401, 422, etc.) + // 429/403/451 are already handled by before(); skip 2xx/3xx. + if ($statusCode < 400 || in_array($statusCode, [429, 403, 451])) { + return; + } + + $fingerprint = $request->getGlobal('rateLimitFingerprint') ?? generateFingerprint(); + $identity = $request->getGlobal('rateLimitIdentity') ?? $this->resolveIdentityFromJwt(); + + $this->limiter->recordIpFailure($fingerprint); + + if ($identity) { + $this->limiter->recordUserFailure($identity, 'jwtApi'); + } + } + + // ------------------------------------------------------------------------- + // HELPERS + // ------------------------------------------------------------------------- + + /** + * Resolve user identity from JWT using your existing helper functions. + * Tries email first, then mobile. Returns null if JWT is absent/invalid. + * + * IMPORTANT: Replace getEmailFromJWT() / getMobileFromJWT() with your + * actual function names if they differ. + */ + protected function resolveIdentityFromJwt(): ?string + { + try { + // Try email from JWT + if (function_exists('getEmailFromJWT')) { + $email = getEmailFromJWT(); + if ($email) { + return strtolower(trim($email)); + } + } + + // Try mobile from JWT + if (function_exists('getMobileFromJWT')) { + $mobile = getMobileFromJWT(); + if ($mobile) { + return trim($mobile); + } + } + } catch (\Throwable $e) { + // JWT invalid or expired — fall through to IP-only limiting + log_message('debug', '[RateLimiter] JWT identity resolution failed: ' . $e->getMessage()); + } + + return null; + } + + /** + * Build and return a JSON response for blocked/throttled requests. + */ + protected function jsonResponse(array $result): ResponseInterface + { + $response = service('response'); + $response->setStatusCode($result['status']); + $response->setContentType('application/json'); + $response->setBody(json_encode([ + 'success' => false, + 'error' => [ + 'code' => strtoupper('RATE_LIMIT_' . $result['level']), + 'message' => $result['message'], + 'type' => $result['type'] ?? 'request', + ], + ])); + return $response; + } +} diff --git a/app/Helpers/HttpRequestHelper.php b/app/Helpers/HttpRequestHelper.php index 3e072e8a..53268b39 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 178ec18a..63351544 100755 --- a/app/Helpers/utility_helper.php +++ b/app/Helpers/utility_helper.php @@ -1069,23 +1069,40 @@ function getRealClientIP() return $request->getIPAddress(); } -function generateFingerprint() +function generateFingerprint(): string { $request = service('request'); $ua = $request->getUserAgent()->getAgentString(); $ip = getRealClientIP(); - // Use only subnet (first 3 blocks) to tolerate IP change - $ipParts = explode('.', $ip); - $ipSubnet = $ipParts[0] . '.' . $ipParts[1] . '.' . $ipParts[2]; + // 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'; + } - // $secret = env('app.sessionFingerprintSalt'); - - // return hash('sha256', $ua . '|' . $ipSubnet . '|' . $secret); - return hash('sha256', $ua . '|' . $ipSubnet ); + return hash('sha256', $ua . '|' . $ipGroup); } + + + if (!function_exists('convertGoogleDriveToDownloadLink')) { function convertGoogleDriveToDownloadLink(?string $url): ?string { diff --git a/app/Libraries/GoogleCalendarService.php b/app/Libraries/GoogleCalendarService.php new file mode 100644 index 00000000..8076a260 --- /dev/null +++ b/app/Libraries/GoogleCalendarService.php @@ -0,0 +1,205 @@ +client = new Client(); + $this->client->setApplicationName($appName); + $this->client->setClientId($clientID); + $this->client->setClientSecret($clientSecret); + $this->client->setRedirectUri($redirectUri); + $this->client->addScope($scopesArray); + $this->client->setPrompt('consent'); + $this->client->setAccessType('offline'); + + $accessToken = session()->get('access_token'); + $refreshToken = session()->get('refresh_token'); // Session-la irunthu refresh token-a edukkurom + + if ($accessToken) { + $this->client->setAccessToken($accessToken); + + if ($this->client->isAccessTokenExpired()) { + if ($refreshToken) { + // Ippo session-la iruntha refresh token-a vechu puthu access token vaangurom + $newToken = $this->client->fetchAccessTokenWithRefreshToken($refreshToken); + + // Romba mukkiyam: Puthu token-la refresh token thirumba varaathu, + // so pazhaya refresh token-aiye namma retain pannanum. + if (!isset($newToken['refresh_token'])) { + $newToken['refresh_token'] = $refreshToken; + } + + session()->set('access_token', $newToken); + $this->client->setAccessToken($newToken); + } else { + // Refresh token illana, user marubadiyum login panna sollanum + return redirect()->to('/google-login'); + } + } + } + } + + public function isReady() + { + return $token = session()->get('access_token') && !$this->client->isAccessTokenExpired(); + } + + public function createEventForOrganizerOnly(array $data) + { + $this->service = new Calendar($this->client); + + // Date kuda '10:00' add panrathu safe + $meeting_start = preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['meeting_date']) ? $data['meeting_date'] . ' 10:00:00' : $data['meeting_date']; + + $event = new Event([ + 'summary' => $data['summary'], + 'description' => $data['description'] ?? '', + 'start' => [ + 'dateTime' => date('c', strtotime($meeting_start)), // Ippo 10 AM-nu fix aagidum + 'timeZone' => 'Asia/Kolkata', + ], + 'end' => [ + 'dateTime' => date('c', strtotime($meeting_start . ' +30 minutes')), + 'timeZone' => 'Asia/Kolkata', + ], + 'reminders' => [ + 'useDefault' => false, + 'overrides' => [ + ['method' => 'email', 'minutes' => 1440], // Munthuna naal 10 AM + ['method' => 'popup', 'minutes' => 60], // Event annaki 9 AM + ], + ], + ]); + + return $this->service->events->insert('primary', $event); + } + + public function createEvent(array $data) + { + $this->service = new Calendar($this->client); + $responses = []; + + try { + + $meetingStartRaw = (preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['meeting_date'])) + ? $data['meeting_date'] . ' 10:00:00' + : $data['meeting_date']; + + $startTime = date('c', strtotime($meetingStartRaw)); + $endTime = date('c', strtotime(date('Y-m-d', strtotime($meetingStartRaw)) . ' 23:59:59')); + + // 2. Prepare Attendees + $attendees = []; + if (!empty($data['emails']) && is_array($data['emails'])) { + foreach ($data['emails'] as $email) { + $attendees[] = ['email' => $email]; + } + } + + // 3. Define the Event + $event = new Event([ + 'summary' => $data['summary'], + 'description' => $data['description'] ?? '', + 'start' => ['dateTime' => $startTime, 'timeZone' => 'Asia/Kolkata'], + 'end' => ['dateTime' => $endTime, 'timeZone' => 'Asia/Kolkata'], + 'attendees' => $attendees, + 'reminders' => [ + 'useDefault' => false, + 'overrides' => [ + ['method' => 'email', 'minutes' => 1440], + ['method' => 'popup', 'minutes' => 60], + ], + ], + ]); + + $optParams = ['sendUpdates' => 'all']; + + $calendarId = 'primary'; + $result = $this->service->events->insert($calendarId, $event, $optParams); + + $responses['status'] = 'success'; + $responses['event_id'] = $result->getId(); + $responses['raw_response'] = $result; + + } catch (\Exception $e) { + + $error_data = [ + 'error' => $e->getMessage(), + 'data' => $data, + 'trace' => $e->getTraceAsString() + ]; + + // Log only on failure + log_message('error', 'Google Calendar Event Creation Failed' . json_encode($error_data ?? [])); + + $responses['status'] = 'error'; + $responses['message'] = $e->getMessage(); + $responses['error_data'] = $error_data; + } + + return $responses; + } + + public function createStaffEvent(array $data) + { + $results = []; + + $meetingStartRaw = (preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['meeting_date'])) + ? $data['meeting_date'] . ' 10:00:00' + : $data['meeting_date']; + + $startTime = date('c', strtotime($meetingStartRaw)); + $endTime = date('c', strtotime(date('Y-m-d', strtotime($meetingStartRaw)) . ' 23:59:59')); + + + // Event Object create panrom (Common for all staff) + $event = new Event([ + 'summary' => $data['summary'], + 'description' => $data['description'] ?? '', + 'start' => ['dateTime' => $startTime, 'timeZone' => 'Asia/Kolkata'], + 'end' => ['dateTime' => $endTime, 'timeZone' => 'Asia/Kolkata'], + 'reminders' => [ + 'useDefault' => false, + 'overrides' => [ + ['method' => 'email', 'minutes' => 1440], + ['method' => 'popup', 'minutes' => 60], + ], + ], + ]); + + // Loop through each staff email in the array + foreach ($data['emails'] as $email) { + try { + + $this->client->setSubject($email); + + $staffService = new Calendar($this->client); + + $results[$email] = $staffService->events->insert('primary', $event); + + } catch (\Exception $e) { + $results[$email] = 'Error: ' . $e->getMessage(); + } + } + + return $results; + } +} \ No newline at end of file diff --git a/app/Libraries/GoogleSheetLib.php b/app/Libraries/GoogleSheetLib.php new file mode 100644 index 00000000..5bdb9464 --- /dev/null +++ b/app/Libraries/GoogleSheetLib.php @@ -0,0 +1,80 @@ +client = new Google_Client(); + + // Service account JSON + $this->client->setAuthConfig( + ROOTPATH . 'gdrive-demo-394007-5b1d856b0c5b.json' + ); + + // IMPORTANT for service account + $this->client->useApplicationDefaultCredentials(); + + // Required scopes + $this->client->addScope([ + Google_Service_Drive::DRIVE, + Google_Service_Sheets::SPREADSHEETS + ]); + + // Init services + $this->sheets = new Google_Service_Sheets($this->client); + $this->drive = new Google_Service_Drive($this->client); + } + + /* ===================== READ ===================== */ + + public function read(string $spreadsheetId, string $range = 'Sheet1') + { + $response = $this->sheets + ->spreadsheets_values + ->get($spreadsheetId, $range); + + return $response->getValues() ?? []; + } + + /* ===================== WRITE ===================== */ + + public function write(string $spreadsheetId, array $values, string $range = 'Sheet1') + { + $body = new Google_Service_Sheets_ValueRange([ + 'values' => $values + ]); + + $this->sheets + ->spreadsheets_values + ->update( + $spreadsheetId, + $range, + $body, + ['valueInputOption' => 'RAW'] + ); + + return true; + } + + /* ===================== DOWNLOAD ===================== */ + + public function downloadExcel(string $spreadsheetId) + { + $response = $this->drive->files->export( + $spreadsheetId, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ['alt' => 'media'] + ); + + return $response->getBody()->getContents(); + } +} diff --git a/app/Libraries/RateLimiterService.php b/app/Libraries/RateLimiterService.php new file mode 100644 index 00000000..80374101 --- /dev/null +++ b/app/Libraries/RateLimiterService.php @@ -0,0 +1,382 @@ +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 + { + // 1. Is the IP already blocked? + $blockInfo = $this->getIpBlock($fingerprint); + 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 hash('sha256', strtolower(trim($identity))); + } +} diff --git a/app/Views/gsheet_editor.php b/app/Views/gsheet_editor.php new file mode 100644 index 00000000..93f40957 --- /dev/null +++ b/app/Views/gsheet_editor.php @@ -0,0 +1,73 @@ + + +
+