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

This commit is contained in:
Gowtham M 2026-02-23 11:45:11 +05:30
commit 44efa7467a
17 changed files with 1335 additions and 28 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

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

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

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

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

@ -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);
}
}
}

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,205 @@
<?php
namespace App\Libraries;
use Google\Client;
use Google\Service\Calendar;
use Google\Service\Calendar\Event;
class GoogleCalendarService
{
protected $client;
protected $service;
public function __construct()
{
$appName = getenv('GOOGLE_OAUTH_APP_NAME');
$clientID = getenv('GOOGLE_OAUTH_CLIENT_ID');
$clientSecret = getenv('GOOGLE_OAUTH_CLIENT_SECRET');
$redirectUri = getenv('GOOGLE_OAUTH_REDIRECT_URI');
$scopes = getenv('GOOGLE_OAUTH_SCOPES');
$scopesArray = explode(',', $scopes);
$this->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;
}
}

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"
}