Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev
This commit is contained in:
commit
44efa7467a
@ -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,
|
||||
|
||||
];
|
||||
|
||||
|
||||
89
app/Config/RateLimiter.php
Normal file
89
app/Config/RateLimiter.php
Normal 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,
|
||||
];
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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([
|
||||
|
||||
66
app/Controllers/GoogleSheetController.php
Normal file
66
app/Controllers/GoogleSheetController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
141
app/Filters/AuthApiRateLimitFilter.php
Normal file
141
app/Filters/AuthApiRateLimitFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
151
app/Filters/JwtApiFilter.php
Normal file
151
app/Filters/JwtApiFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ class HttpRequestHelper
|
||||
|
||||
$data = [
|
||||
'ip' => $request->getIPAddress(),
|
||||
'real_ip' => getRealClientIP(),
|
||||
'platform' => $platform,
|
||||
'browser' => $browser,
|
||||
'method' => strtoupper($request->getMethod()),
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
205
app/Libraries/GoogleCalendarService.php
Normal file
205
app/Libraries/GoogleCalendarService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
80
app/Libraries/GoogleSheetLib.php
Normal file
80
app/Libraries/GoogleSheetLib.php
Normal 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();
|
||||
}
|
||||
}
|
||||
382
app/Libraries/RateLimiterService.php
Normal file
382
app/Libraries/RateLimiterService.php
Normal 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)));
|
||||
}
|
||||
}
|
||||
73
app/Views/gsheet_editor.php
Normal file
73
app/Views/gsheet_editor.php
Normal 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>
|
||||
13
gdrive-demo-394007-5b1d856b0c5b.json
Normal file
13
gdrive-demo-394007-5b1d856b0c5b.json
Normal 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"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user