Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev

This commit is contained in:
VENKATESHWARAN 2026-02-21 15:30:18 +05:30
commit d0f3a839f9
17 changed files with 1108 additions and 79 deletions

View File

@ -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,
];

View File

@ -0,0 +1,89 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class RateLimiter extends BaseConfig
{
/*
|--------------------------------------------------------------------------
| JWT / Authenticated API Routes
|--------------------------------------------------------------------------
*/
public array $jwtApi = [
'limit' => 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,
];
}

View File

@ -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
*/

View File

@ -688,51 +688,4 @@ class ApiServiceController extends BaseController
// Get Claims
public function getClaims($id)
{
if ($id == 1) {
$controller = new VidalApiController();
} elseif ($id == 2) {
$controller = new ICICILombardController();
} elseif ($id == 3) {
$controller = new MediAssistApiController();
}
}
// Get UHID
public function getUhid($id)
{
if ($id == 1) {
$controller = new VidalApiController();
} elseif ($id == 2) {
$controller = new ICICILombardController();
} elseif ($id == 3) {
$controller = new MediAssistApiController();
}
}
// Push Enrollment
public function pushEnrollment($id)
{
if ($id == 1) {
$controller = new VidalApiController();
} elseif ($id == 2) {
$controller = new ICICILombardController();
} elseif ($id == 3) {
$controller = new MediAssistApiController();
}
}
// Get Hospital Network
public function getHospitalNetwork($id)
{
if ($id == 1) {
$controller = new VidalApiController();
} elseif ($id == 2) {
$controller = new ICICILombardController();
} elseif ($id == 3) {
$controller = new MediAssistApiController();
}
}
}

View File

@ -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

View File

@ -0,0 +1,66 @@
<?php namespace App\Controllers;
use App\Libraries\GoogleSheetLib;
use CodeIgniter\Controller;
class GoogleSheetController extends Controller
{
protected GoogleSheetLib $sheetLib;
public function __construct()
{
$this->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);
}
}

View File

@ -79,7 +79,7 @@ class HealthIndiaApiController extends BaseController
]);
}
public function SubmitClaim($claimId = 729)
public function SubmitClaim($claimId = null)
{
helper('api');
@ -222,9 +222,9 @@ class HealthIndiaApiController extends BaseController
$this->db->table('ticket_master')
->where('id', $claimId)
->update([
'claim_number' => $ccn,
'tpa_claim_id' => $ccn,
'tpa_claim_push_reference_no' => $ccn . '(' . $ccnExt . ')',
// 'claim_number' => $ccn,
// 'tpa_claim_id' => $ccn,
'tpa_claim_push_reference_no' => $ccn . '-' . $ccnExt,
'updated_at' => date('Y-m-d H:i:s')
]);
@ -237,14 +237,14 @@ class HealthIndiaApiController extends BaseController
return;
}
public function ClaimDetail($claimId = 729)
public function ClaimDetail($claimId = null)
{
helper('api');
log_message('error', 'HEALTH_INDIA - Claim Status | Started for claimId: ' . $claimId);
$ticket = $this->db->table('ticket_master tm')
->select("tm.id, tm.tpa_claim_id as ccn, cp.policy_no, cp.policy_start_date, cp.policy_end_date, tm.emp_code")
->select("tm.id, tm.tpa_claim_push_reference_no, cp.policy_no, cp.policy_start_date, cp.policy_end_date, tm.emp_code")
->join('client_policy cp', 'tm.client_policy_id=cp.id')
->where('tm.id', $claimId)
->get()->getRowArray();
@ -267,10 +267,14 @@ class HealthIndiaApiController extends BaseController
$url = getenv('HEALTH_INDIA_BASE_URL') . "/Claims/GetClaims";
// Using claim number wise approach (Section 7.3 - option 3)
$reference = $ticket['tpa_claim_push_reference_no'];
$parts = explode('-', $reference);
$ccn = $parts[0] ?? null;
$ccnExt = $parts[1] ?? null;
$body = [
"policY_NUMBER" => $ticket['policy_no'],
"CCN" => $ticket['ccn'],
"CCN_EXT" => "0"
"CCN" => $ccn,
"CCN_EXT" => $ccnExt
];
$headers = [
@ -309,6 +313,8 @@ class HealthIndiaApiController extends BaseController
$updateArray = [
'tpa_claim_status' => $currentStatus,
'tpa_claim_id' => $tpa_claim_no,
'claim_number' => $tpa_claim_no,
'updated_at' => date('Y-m-d H:i:s'),
];
if (isset($validStatuses[$currentStatus])) {

View File

@ -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()

View File

@ -506,6 +506,8 @@ class MediAssistApiController extends BaseController
tm.id,
tm.tpa_no as memberId,
tm.tpa_claim_push_reference_no as claimRefNo,
tm.tpa_claim_id,
tm.claim_number,
cp.policy_no as policyNo,
cp.policy_start_date as startDate,
cp.policy_end_date as endDate,
@ -521,6 +523,7 @@ class MediAssistApiController extends BaseController
return ['status' => false,'message' => 'Invalid Claim ID' ];
}
// REQUEST BODY
if($ticket['claimRefNo'] != null)
{
@ -617,13 +620,18 @@ class MediAssistApiController extends BaseController
$updateArray = [
'tpa_claim_status' => $currentStatus,
'tpa_claim_id' => $tpa_claim_no,
'claim_number' => $tpa_claim_no,
'updated_at' => date('Y-m-d H:i:s'),
];
if (isset($validStatuses[$currentStatus])) {
$updateArray['claim_status_id'] = $validStatuses[$currentStatus];
}
if (empty($ticket['tpa_claim_id']) || $ticket['tpa_claim_id'] === null) {
$updateArray['tpa_claim_id'] = $tpa_claim_no;
}
if (empty($ticket['claim_number']) || $ticket['claim_number'] === null) {
$updateArray['claim_number'] = $tpa_claim_no;
}
if (!empty($tpa_claim_type)) {
$updateArray['tpa_claim_type'] = $tpa_claim_type;
}

View File

@ -0,0 +1,141 @@
<?php
namespace App\Filters;
use App\Libraries\RateLimiterService;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
/**
* AuthApiFilter
*
* Applied to API routes that do NOT use JWT e.g. verifyMobileNumber, verifyOTP.
* Identity is extracted from request params: 'email' or 'mobile_number'.
*
* Performs:
* - IP-level throttle + progressive block check (via fingerprint)
* - User-level block check (if identity present in params)
*
* Usage in Routes.php:
* $routes->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;
}
}

View File

@ -0,0 +1,151 @@
<?php
namespace App\Filters;
use App\Libraries\RateLimiterService;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
/**
* JwtApiFilter
*
* Applied to API routes that require a valid JWT token.
* Identity (email or mobile) is extracted from the JWT payload using
* your existing helper functions: getEmailFromJWT() / getMobileFromJWT().
*
* Performs:
* - IP-level throttle + progressive block check (via fingerprint)
* - User-level throttle + progressive block check (by JWT identity)
*
* Usage in Routes.php:
* $routes->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;
}
}

View File

@ -15,6 +15,7 @@ class HttpRequestHelper
$data = [
'ip' => $request->getIPAddress(),
'real_ip' => getRealClientIP(),
'platform' => $platform,
'browser' => $browser,
'method' => strtoupper($request->getMethod()),

View File

@ -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
{

View File

@ -0,0 +1,80 @@
<?php namespace App\Libraries;
use Google_Client;
use Google_Service_Sheets;
use Google_Service_Drive;
use Google_Service_Sheets_ValueRange;
class GoogleSheetLib
{
protected Google_Client $client;
protected Google_Service_Sheets $sheets;
protected Google_Service_Drive $drive;
public function __construct()
{
$this->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();
}
}

View File

@ -0,0 +1,382 @@
<?php
namespace App\Libraries;
use Config\RateLimiter as RateLimiterConfig;
use CodeIgniter\Cache\CacheInterface;
/**
* RateLimiterService
*
* Handles all rate limiting logic:
* - IP-level throttle + progressive blocking (soft/medium/hard)
* - User-level progressive blocking (soft/medium/hard) by email or mobile_number
* - Manual block / unblock helpers callable from anywhere
*
* Block levels: 'soft' | 'medium' | 'hard'
* All blocks are MANUAL UNBLOCK ONLY (no auto-expiry on block state).
* Counters and violation counts use cache TTLs; block records do not expire.
*/
class RateLimiterService
{
protected RateLimiterConfig $config;
protected CacheInterface $cache;
public function __construct()
{
$this->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)));
}
}

View File

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<title>Google Sheet Editor</title>
<style>
body { font-family: Arial; padding: 10px; }
table { border-collapse: collapse; width: 100%; }
td { border: 1px solid #ccc; padding: 6px; min-width: 80px; }
td[contenteditable] { background: #fffde7; }
button { margin-right: 10px; }
</style>
</head>
<body>
<h3>Google Sheet Editor</h3>
<button onclick="save()">💾 Save</button>
<button onclick="download()"> Download</button>
<hr>
<table id="sheet"></table>
<script>
alert('first');
const sheetId = "<?= esc($sheetId) ?>";
alert('second');
/* ---------- LOAD ---------- */
fetch('<?php echo base_url() ?>' + `/sheet/${sheetId}/fetch`)
.then(r => r.json())
.then(r => r.json())
.then(render);
function render(data) {
alert('data');
console.log('data');
console.log(data);
const table = document.getElementById('sheet');
table.innerHTML = '';
data.forEach(row => {
const tr = document.createElement('tr');
row.forEach(cell => {
const td = document.createElement('td');
td.contentEditable = true;
td.innerText = cell ?? '';
tr.appendChild(td);
});
table.appendChild(tr);
});
}
/* ---------- SAVE ---------- */
function save() {
const rows = [...document.querySelectorAll('tr')]
.map(tr => [...tr.children].map(td => td.innerText));
fetch(`/sheet/${sheetId}/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(rows)
})
.then(() => alert('Saved successfully'));
}
/* ---------- DOWNLOAD ---------- */
function download() {
window.location.href = '<?php echo base_url() ?>' + `/sheet/${sheetId}/download`;
}
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "gdrive-demo-394007",
"private_key_id": "5b1d856b0c5b13e52b5210d381ce7ae02204f666",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9tqkwe8XEuvDJ\ngDAKHn7FzFgkmor9sEZPkjofVJ1dK0RpD1mMVw38BzzMsEo8Y8aojNKj9FcgJPI+\nMiSwvoHDVGxPuyz2Q7o8BS7WdM83Sn69CBGn+0s+YjsyQ5pQBeu1YZ3erjckyA1M\nX0a0qXSFRm7dN0pwDIk0SP9/pl5iAKEtakuXn/Q+lSIpHz6OUgQy8bCjn91poMtF\nhL1YR/7k8ZttjQiFSCTzQ6e3IJmVbqY9UZ2hc4zVicLVSLM2x17M1CCVrsXoceOj\nQoYEeCqP2qJjGA29CB66xpXetuOFcll/ZHiNBhSekOBbJIKFG8WfTKaFn4hu2HX6\nzbJLse5FAgMBAAECggEAXK8qxWcS3eQ+0xLvZWI0qUoGHgvqr7o4/5L/FmNuZiBH\nUdSP+UJmsKSQjafq/Mn6OkpidntfPXMPbldtGXRZTSanq+RUORQpnj0h/uAehHK+\nrHeOuLTKs/Wl2g6xCzt5RqokSLBwfGXIKXG6x3SqWppoe2cR1OArAAJR4PlUzyeM\nP0HkZcVMDrXkshpbi/7yk1Yol5CTJjUrXT4cH2eFSih+eu5UxI/uEdxu86XnaB+V\nBDS94nSQffaMem3YLRSQpPWMHJts3NM2eoxpVy3NqbyHH0Jzr47T5+pdnk5AZX8v\nMu7L0FYgUmGli8W9/jV43lUi9z147EpNC1ugySICGQKBgQDeT3i9mBO1HQB+3y05\n4ao9YKWtR7RHTIiMBkekRs58xxWIq75+bsJtLMy0GsHOJHbpdfDU2AtcasYXVm/L\nc960AR7Qm0mdhQi5CG7XfFTvkc+RhsFoCAYOnbdInr1D+s7NsyOgdYvzh9HYIiJ/\nm8MzeGQia/4tlO+UNq7UK0sfowKBgQDadpe0OCynXEiziSY/aBT9mrjplXJSJUeR\nX5/pUrBV++mYt+LJU0Q+4op0Qf+PUJwp72O1v3T9h4ox2BcrYUMI17dJZ5HG46BG\n6Sjh+mZzLCTN9L6AVgRzK/5CUpsL+oPpClzGQK+1uJ+YKZD086YBuQi2JiG6uBxk\n7VLzATZ49wKBgQC3ZHgGb95SGoq+Hv4AMdluqLwEJpLh/pDmcofHTWIqLVHmXUfY\npSZfSgXUzf3zQMGX9mOmMlOs+ahQuE2hWQTvGb2B+ZjRCV4Yxowp17d5qp/BPZlv\naK8Wf6Ujk1AvNEhGCPHq/Q1m6TSDSCWNf8GYREjW3J/immrJqhKvlMd0YQKBgQCM\nf2CpQsdVCwCmljnG5YU6ZFsvvjE7q0YPtFP/lnJZmh1tXjW4DJkDaGZqxlc5MDp+\nrbqOlIcE1jqGO9cKyw51jWYPC1CxfIsDj8f/LS7eOzGgUxqBJtDN0SlANigI2CAl\nq8hmqAtY71eUYIcdQeUtjnaPzo46q1V3gzmplsoVmQKBgDCzaBbFY1bKW+xPPC98\nutYS7cGYtTq241zafSXx/qmpZB2QwFsR3rmZ9msCEBb3TY4Ew+hUi7+SP3ycSU1s\nkwIO/9DaZhFBRA9jbwbu4zdB2Niamfo79epqJ8vhJ6TA2d5xRSGbHWkGymWV/e0p\na74mh5hQ0lqI4MSeArFrJwwF\n-----END PRIVATE KEY-----\n",
"client_email": "gsheet@gdrive-demo-394007.iam.gserviceaccount.com",
"client_id": "108808196910972902964",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/gsheet%40gdrive-demo-394007.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}