From 8f91f821af5938cedf120497b604a263a5eef54d Mon Sep 17 00:00:00 2001 From: velz Date: Fri, 21 Nov 2025 15:54:45 +0530 Subject: [PATCH 1/4] FEAT_FE_DEPLOY --- app/Config/Routes.php | 2 + app/Controllers/DeployController.php | 286 +++++++++++++++++++++++++++ app/Views/fedeploy.php | 43 ++++ 3 files changed, 331 insertions(+) create mode 100644 app/Controllers/DeployController.php create mode 100644 app/Views/fedeploy.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d2a077fd..4e8cab49 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -12,6 +12,8 @@ $routes->get('/chatbottest', 'ChatbotControllerNew::chatbotest'); $routes->get('/chatbot', 'ChatbotControllerNew::chatbot'); $routes->get('/swagger', 'SwaggerController::index', ['filter' => 'authMVC']); +$routes->get('/fedeploy', 'DeployController::fedeploy_view', ['filter' => 'authMVC']); +$routes->post('/fedeploy', 'DeployController::fedeploy', ['filter' => 'authMVC']); // Reminder Mail Notification diff --git a/app/Controllers/DeployController.php b/app/Controllers/DeployController.php new file mode 100644 index 00000000..de73092a --- /dev/null +++ b/app/Controllers/DeployController.php @@ -0,0 +1,286 @@ +myLogger = \Config\Services::mylogger(); + } + // public function fedeploy() + // { + // $request = $this->request; + + // // Multiple zip files: + // // $zipFiles = $this->request->getFileMultiple('zip_files'); + // $zipFiles = $this->request->getFile('zip_files');//('zip_files'); + // // print_r($zipFiles);die(); + + // // Matching dropdown/text values (arrays, indexed same as zip_files[]) + // $zipFolders = (array) $request->getPost('zip_folder'); // e.g. ["web/", "dist/"] + // $s3Buckets = (array) $request->getPost('s3_bucket'); // e.g. ["benefits-app-bucket", ...] + // $s3Prefixes = (array) $request->getPost('s3_prefix'); // e.g. ["hr/", "payroll/"] + // $cfDistributions = (array) $request->getPost('cf_distribution_id'); // e.g. ["DIST1", "DIST1", "DIST2"] + // $cfPathsList = (array) $request->getPost('cf_paths'); // e.g. ["/hr/*", "/hr/special/*", "/payroll/*"] + + // // Path to Python interpreter and deploy_assets.py script + // $pythonBin = getenv('PY_PATH'); // adjust if needed + // $scriptPath = getenv('PY_SCRIPT_PATH'); // full path to your script + + // $results = []; + + // if (empty($zipFiles)) { + // return $this->response->setJSON([ + // 'status' => 'error', + // 'message' => 'No zip files uploaded', + // ]); + // } + + // // -------------------------------------------------------- + // // 1) Pre-process distribution IDs → group by distribution + // // - So we can run ONE invalidation per distribution + // // - And merge all paths into a single JSON array + // // -------------------------------------------------------- + // $distGroups = []; // [distId => ['indexes' => [...], 'paths' => [...], 'firstIndex' => int]] + + // foreach ($cfDistributions as $i => $distId) { + // $distId = trim((string) $distId); + // $path = trim((string) ($cfPathsList[$i] ?? '')); + + // // Only consider entries where both distribution ID and path are set + // if ($distId === '' || $path === '') { + // continue; + // } + + // if (!isset($distGroups[$distId])) { + // $distGroups[$distId] = [ + // 'indexes' => [], + // 'paths' => [], + // ]; + // } + + // $distGroups[$distId]['indexes'][] = $i; + // $distGroups[$distId]['paths'][] = $path; + // } + + // // Set firstIndex and unique paths per distribution + // foreach ($distGroups as $distId => $info) { + // $distGroups[$distId]['firstIndex'] = $info['indexes'][0]; // first file index for this dist + // $distGroups[$distId]['paths'] = array_values(array_unique($info['paths'])); // unique paths + // } + + // // -------------------------------------------------------- + // // 2) Process each uploaded file + // // -------------------------------------------------------- + // foreach ($zipFiles as $index => $file) { + // if (!$file->isValid() || $file->hasMoved()) { + // $results[] = [ + // 'file' => $file->getName(), + // 'status' => 'error', + // 'message' => 'Invalid file or already moved.', + // ]; + // continue; + // } + + // // Read matching form values (or fallback) + // $zipFolder = $zipFolders[$index] ?? 'web/'; // default example + // $s3Bucket = $s3Buckets[$index] ?? ''; + // $s3Prefix = $s3Prefixes[$index] ?? ''; + // $cfDistribution = $cfDistributions[$index] ?? ''; + // $cfDistribution = trim((string) $cfDistribution); + + // // Move file into a known directory (keep original name) + // // $uploadDir = WRITEPATH . 'uploads/deploy'; + // $uploadDir = '/home/ubuntu/py'; + // if (!is_dir($uploadDir)) { + // mkdir($uploadDir, 0775, true); + // } + + // $originalName = $file->getName(); + // $file->move($uploadDir, $originalName); + // $zipPath = $uploadDir . DIRECTORY_SEPARATOR . $originalName; + + // // -------------------------------------------------------- + // // Decide whether THIS file should trigger invalidation + // // - Only firstIndex of each distribution will do it + // // - cf-paths-json = JSON array of all unique paths for that dist + // // -------------------------------------------------------- + // $doInvalidation = false; + // $cfPathsJson = ''; + + // if ($cfDistribution !== '' && isset($distGroups[$cfDistribution])) { + // $group = $distGroups[$cfDistribution]; + + // if ($group['firstIndex'] === $index) { + // // This is the first file for this distribution ID → do invalidation ONCE + // $doInvalidation = true; + + // // Build CloudFront paths JSON array (AWS syntax: ["path1", "path2", ...]) + // $cfPathsJson = json_encode($group['paths']); // e.g. ["\/hr\/*","\/hr\/special\/*"] + // } + // // Else: same distribution ID but NOT first index → no invalidation + // } + + // // -------------------------------------------------------- + // // Build the python command safely using escapeshellarg + // // -------------------------------------------------------- + // $cmdParts = [ + // escapeshellarg($pythonBin), + // escapeshellarg($scriptPath), + // '--zip-path', escapeshellarg($zipPath), + // '--zip-folder', escapeshellarg($zipFolder), + // '--s3-bucket', escapeshellarg($s3Bucket), + // '--s3-prefix', escapeshellarg($s3Prefix), + // '--do-webhook', // always send webhook after success + // ]; + + // if ($doInvalidation) { + // $cmdParts[] = '--do-invalidation'; + // $cmdParts[] = '--cf-distribution-id'; + // $cmdParts[] = escapeshellarg($cfDistribution); + // $cmdParts[] = '--cf-paths-json'; + // $cmdParts[] = escapeshellarg($cfPathsJson); // e.g. '["/hr/*","/hr/special/*"]' + // } + + // // Final command string (2>&1 to capture stderr too) + // $cmd = implode(' ', $cmdParts) . ' 2>&1'; + // print_r($cmd);die(); + // $output = []; + // $returnVar = 0; + + // exec($cmd, $output, $returnVar); + + // $results[] = [ + // 'file' => $originalName, + // 'zip_path' => $zipPath, + // 'command' => $cmd, + // 'output' => $output, + // 'exit_code' => $returnVar, + // 'status' => $returnVar === 0 ? 'success' : 'error', + // ]; + // } + + // return $this->response->setJSON([ + // 'status' => 'completed', + // 'results' => $results, + // ]); + // } + + public function fedeploy() + { + $request = $this->request; + + // Single zip file: + $file = $request->getFile('zip_file'); + + if (!$file || !$file->isValid()) { + return $this->response->setJSON([ + 'status' => 'error', + 'message' => 'No valid zip file uploaded', + ]); + } + + // Read scalar form values + $zipFolder = (string) $request->getPost('zip_folder') ?: 'web/'; + $s3Bucket = (string) $request->getPost('s3_bucket') ?: ''; + $s3Prefix = (string) $request->getPost('s3_prefix') ?: ''; + $cfDistribution = trim((string) $request->getPost('cf_distribution_id')); + $cfPathsRaw = (string) $request->getPost('cf_paths'); + + // Python interpreter and script path from env + $pythonBin = getenv('PY_PATH'); // e.g. /usr/bin/python3 + $scriptPath = getenv('PY_SCRIPT_PATH'); // e.g. /home/ubuntu/deploy_assets.py + + if (empty($pythonBin) || empty($scriptPath)) { + return $this->response->setJSON([ + 'status' => 'error', + 'message' => 'Python path or script path not configured in environment.', + ]); + } + + // Move file into known directory (keep original name) + $uploadDir = '~/py'; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0775, true); + } + + $originalName = $file->getName(); + $file->move($uploadDir, $originalName); + $zipPath = $uploadDir . DIRECTORY_SEPARATOR . $originalName; + + // ----------------------------- + // CloudFront invalidation data + // ----------------------------- + $doInvalidation = false; + $cfPathsJson = ''; + + // Allow comma or newline separated paths: "/hr/*,/hr/special/*" or + // "/hr/*\n/hr/special/*" + $paths = array_filter( + array_map('trim', preg_split('/[\r\n,]+/', $cfPathsRaw)), + 'strlen' + ); + + if ($cfDistribution !== '' && !empty($paths)) { + $doInvalidation = true; + $cfPathsJson = json_encode($paths); // e.g. ["\/hr\/*","\/hr\/special\/*"] + } + + // ----------------------------- + // Build python command + // ----------------------------- + $cmdParts = [ + escapeshellarg($pythonBin), + escapeshellarg($scriptPath), + '--zip-path', escapeshellarg($zipPath), + '--zip-folder', escapeshellarg($zipFolder), + '--s3-bucket', escapeshellarg($s3Bucket), + '--s3-prefix', escapeshellarg($s3Prefix), + '--do-webhook', // always send webhook after success + ]; + + if ($doInvalidation) { + $cmdParts[] = '--do-invalidation'; + $cmdParts[] = '--cf-distribution-id'; + $cmdParts[] = escapeshellarg($cfDistribution); + $cmdParts[] = '--cf-paths-json'; + $cmdParts[] = escapeshellarg($cfPathsJson); + } + + // Final command string (2>&1 to capture stderr too) + $cmd = implode(' ', $cmdParts) . ' 2>&1'; + $output = []; + $returnVar = 0; + print_r($cmd);die(); + exec($cmd, $output, $returnVar); + + $result = [ + 'file' => $originalName, + 'zip_path' => $zipPath, + 'command' => $cmd, + 'output' => $output, + 'exit_code' => $returnVar, + 'status' => $returnVar === 0 ? 'success' : 'error', + ]; + + return $this->response->setJSON([ + 'status' => 'completed', + 'result' => $result, + ]); + } + + + public function fedeploy_view() + { + // $this->load->view('fedeploy'); + $this->loadLayout('fedeploy'); + } + +} diff --git a/app/Views/fedeploy.php b/app/Views/fedeploy.php new file mode 100644 index 00000000..906bf5f7 --- /dev/null +++ b/app/Views/fedeploy.php @@ -0,0 +1,43 @@ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ + +
From 0cfd3d64bc1bdfbbab1572255cc3066269ec67c1 Mon Sep 17 00:00:00 2001 From: velz Date: Fri, 21 Nov 2025 19:15:39 +0530 Subject: [PATCH 2/4] FIX_BDS_ID_REPLACE_WITH_PPID --- app/Controllers/PayoutController.php | 5 ++++- app/Models/InvoiceModel.php | 10 ++++++---- app/Views/invoice_policy_mapping.php | 3 ++- app/Views/invoice_policy_mapping_add.php | 5 ++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/Controllers/PayoutController.php b/app/Controllers/PayoutController.php index e2479e42..f00ec448 100644 --- a/app/Controllers/PayoutController.php +++ b/app/Controllers/PayoutController.php @@ -221,6 +221,8 @@ class PayoutController extends BaseController if ($type === 'add') { // $invoiceNo = $this->generateInvoiceNumber(); $data['payouts'] = $this->invoiceModel->payoutList(1); + // print_rr($data['payouts']);die(); + // print_rr($this->invoiceModel->getLastQuery());die(); // $data['invoice_number'] = $invoiceNo; return $this->loadLayout('invoice_policy_mapping_add', $data); } @@ -260,6 +262,7 @@ class PayoutController extends BaseController public function saveInvoice() { $json = $this->request->getJSON(true); + // print_rr($json);die(); if (!$json) { return $this->response->setJSON(['error' => 'Invalid JSON','message' => 'Invalid JSON received.'])->setStatusCode(400); @@ -293,7 +296,7 @@ class PayoutController extends BaseController foreach ($json['policies'] as $p) { $this->invoiceItemModel->insert([ 'invoice_id' => $invoiceId, - 'policy_id' => $p['policy_id'], + 'policy_id' => $p['partner_policy_id'], 'policy_no' => $p['policy_no'], 'commission_amount' => $p['commission_amount'], 'is_active' => 1 diff --git a/app/Models/InvoiceModel.php b/app/Models/InvoiceModel.php index d1c312f4..80a47e69 100644 --- a/app/Models/InvoiceModel.php +++ b/app/Models/InvoiceModel.php @@ -203,17 +203,19 @@ class InvoiceModel extends Model DATE_FORMAT(pp.issued_date, "%d/%m/%Y") AS date, pii.id AS invoiceItemId, pii.commission_amount as paid_amount, - pi.payout_status + pi.payout_status, + pp.id as partner_policy_id, + pp.policy_transaction_id ') ->join('partner_invoice_items pii','pii.policy_no = pt.policy_no','left') ->join('partner_invoice pi','pi.id = pii.invoice_id','left') - ->join('partner_policy pp','pt.policy_no = pp.policy_number AND pt.agent_id = pp.agent_id','left') + ->join('partner_policy pp','pt.policy_no = pp.policy_number AND pt.agent_id = pp.agent_id AND pt.id = pp.policy_transaction_id') ->where('pt.is_active',1) - ->where('pt.agent_id IS NOT NULL', null, false); + ->where('pt.agent_id IS NOT NULL'); if ($flag == 1) { // Add mode // EXCLUDE all policies that exist in partner_invoice_items - $builder->where("pt.policy_no NOT IN (SELECT policy_no FROM partner_invoice_items)", null, false); + $builder->where("pt.id NOT IN (SELECT policy_id FROM partner_invoice_items)", null, false); } if ($flag == 2) { // Edit mode diff --git a/app/Views/invoice_policy_mapping.php b/app/Views/invoice_policy_mapping.php index 870bcd66..3741d1d8 100644 --- a/app/Views/invoice_policy_mapping.php +++ b/app/Views/invoice_policy_mapping.php @@ -673,7 +673,8 @@ table.dataTable tbody td { padding: 4px 4px !important; } return { policy_id: p.id, policy_no: p.policyNo, - commission_amount: finalAmount + commission_amount: finalAmount, + partner_policy_id : p.partner_policy_id }; }); diff --git a/app/Views/invoice_policy_mapping_add.php b/app/Views/invoice_policy_mapping_add.php index ee5a23f2..6a604ebf 100755 --- a/app/Views/invoice_policy_mapping_add.php +++ b/app/Views/invoice_policy_mapping_add.php @@ -496,6 +496,8 @@ table.dataTable tbody td { padding: 4px 4px !important; } const selectedPolicyData = filteredPolicies.filter(p => selectedPolicies.has(String(p.id))); + console.log('selectedPolicyData'); + console.log(selectedPolicyData); let totalAmount = 0; const policies = selectedPolicyData.map(p => { @@ -504,7 +506,8 @@ table.dataTable tbody td { padding: 4px 4px !important; } return { policy_id: p.id, policy_no: p.policyNo, - commission_amount: finalAmount + commission_amount: finalAmount, + partner_policy_id : p.partner_policy_id }; }); From 16d658b2955142e6ca02cf1eb5cb710d54c4bd8f Mon Sep 17 00:00:00 2001 From: velz Date: Mon, 24 Nov 2025 10:48:04 +0530 Subject: [PATCH 3/4] FIX_RULES_FIELDS --- app/Controllers/RuleImportController.php | 13 +++++++++++++ app/Views/invoice_policy_mapping_add.php | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/Controllers/RuleImportController.php b/app/Controllers/RuleImportController.php index 91fc2ddc..1fc562b9 100644 --- a/app/Controllers/RuleImportController.php +++ b/app/Controllers/RuleImportController.php @@ -42,6 +42,19 @@ class RuleImportController extends AdminController 'is_new_vehicle', 'cubic_capacity', 'policy_business_type', + 'fuel_type', + 'produt', + 'geo_rto_state', + 'geo_rto_city', + 'model', + 'make', + 'weight', + 'renewal_type', + 'renewal_sub_type', + 'premium', + 'od_premium', + 'tp_premium', + 'product' ], ]; diff --git a/app/Views/invoice_policy_mapping_add.php b/app/Views/invoice_policy_mapping_add.php index ca804873..a49cf30c 100755 --- a/app/Views/invoice_policy_mapping_add.php +++ b/app/Views/invoice_policy_mapping_add.php @@ -276,7 +276,7 @@ table.dataTable tbody td { padding: 4px 4px !important; } let agentId = agentEl ? agentEl.value : ''; let policyTillDate = policyTillDateEl ? policyTillDateEl.value : ''; - + console.log('policyTillDate', policyTillDate); selectedPolicies.clear(); if (!agentId || agentId === '' || agentId === '0') { From bcbe96ad0d3b567255e9cd90ba54a54fc4cb801b Mon Sep 17 00:00:00 2001 From: velz Date: Tue, 25 Nov 2025 09:27:32 +0530 Subject: [PATCH 4/4] FEAT_CORS --- app/Config/Filters.php | 8 +- app/Controllers/PayoutController.php | 12 +- app/Filters/Cors.php | 422 +++++++++++++++++++++++++++ app/Models/InvoiceModel.php | 7 +- 4 files changed, 442 insertions(+), 7 deletions(-) create mode 100644 app/Filters/Cors.php diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 00faa273..029d2706 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -14,6 +14,7 @@ use App\Filters\HttpRequestLog; use App\Filters\CloseDbConnection; use App\Filters\AuthClientApi; use App\Filters\CommissionApiFilter; +use App\Filters\Cors; use App\Filters\AuthJWT; @@ -38,7 +39,8 @@ class Filters extends BaseConfig 'authJWT' => AuthJWT::class, 'AuthClientApi' => AuthClientApi::class, 'CloseDbConnection' => CloseDbConnection::class, - 'CommissionApiFilter' => CommissionApiFilter::class + 'CommissionApiFilter' => CommissionApiFilter::class, + 'Cors' => Cors::class ]; /** @@ -51,11 +53,13 @@ class Filters extends BaseConfig public array $globals = [ 'before' => [ 'HttpRequestLog' => ['except' => 'cli/*'], + 'Cors', // 'csrf', // 'invalidchars', ], 'after' => [ - 'CloseDbConnection' + 'CloseDbConnection', + 'Cors', // 'secureheaders', ], ]; diff --git a/app/Controllers/PayoutController.php b/app/Controllers/PayoutController.php index c4b3708b..7dccb5ba 100644 --- a/app/Controllers/PayoutController.php +++ b/app/Controllers/PayoutController.php @@ -212,8 +212,14 @@ class PayoutController extends BaseController $id = $this->request->getGet('id'); - - $data['agents'] = $this->invoiceModel->agentList(); // common both add and edit + if($type == 'add') + { + $data['agents'] = $this->invoiceModel->agentList(['is_active' => 1]); // common both add and edit + } + else + { + $data['agents'] = $this->invoiceModel->agentList(); // common both add and edit + } // $data['checked_policy_numbers'] = []; // $data['invoice'] = []; // $data['extra_payouts'] = []; @@ -253,7 +259,7 @@ class PayoutController extends BaseController $data['invoice_number']= $invoice['invoice_no']; $data['type'] = $type; $data['payout_status'] = $invoice['payout_status'] ; - + // dd($data); return $this->loadLayout('invoice_policy_mapping', $data); } diff --git a/app/Filters/Cors.php b/app/Filters/Cors.php new file mode 100644 index 00000000..c47f2762 --- /dev/null +++ b/app/Filters/Cors.php @@ -0,0 +1,422 @@ + + */ + protected array $allowedOrigins = []; + + /** + * Whether to allow credentials (cookies, authorization headers) in CORS requests + * WARNING: Cannot be true if using wildcard (*) origin + * + * @var bool + */ + protected bool $allowCredentials = false; + + /** + * HTTP methods allowed for CORS requests + * + * @var string + */ + protected string $allowedMethods = 'GET,POST,PUT,PATCH,DELETE,OPTIONS'; + + /** + * HTTP headers allowed in CORS requests + * + * @var string + */ + protected string $allowedHeaders = 'Content-Type,Authorization,X-Requested-With,Accept,Origin'; + + /** + * Headers exposed to the client (accessible via JavaScript) + * + * @var string + */ + protected string $exposeHeaders = ''; + + /** + * How long (in seconds) the preflight response can be cached + * Default: 24 hours (86400 seconds) + * + * @var int + */ + protected int $maxAge = 86400; + + /** + * Whether to enable debug logging for CORS requests + * + * @var bool + */ + protected bool $debug = false; + + /** + * Initialize CORS configuration from environment variables + * + * @throws \RuntimeException If configuration is invalid + */ + + protected $myLogger; + public function __construct() + { + + $this->myLogger = \Config\Services::mylogger(); + // Parse allowed origins from environment variable + // Format: comma or semicolon separated list + // Examples: "https://example.com,https://app.example.com" or "*.example.com" + $raw = env('CORS_ALLOWED_ORIGINS', '*'); + $parts = preg_split('/\s*[,;]\s*/', trim($raw)); + $this->allowedOrigins = array_filter(array_map('trim', $parts)); + + // Load other configuration from environment + $this->allowCredentials = filter_var( + env('CORS_ALLOW_CREDENTIALS', false), + FILTER_VALIDATE_BOOLEAN + ); + $this->allowedMethods = env('CORS_ALLOWED_METHODS', $this->allowedMethods); + $this->allowedHeaders = env('CORS_ALLOWED_HEADERS', $this->allowedHeaders); + $this->exposeHeaders = env('CORS_EXPOSE_HEADERS', $this->exposeHeaders); + $this->maxAge = (int) env('CORS_MAX_AGE', $this->maxAge); + $this->debug = filter_var(env('CORS_DEBUG', false), FILTER_VALIDATE_BOOLEAN); + + // Security validation: wildcard origin cannot be used with credentials + // This is a browser security requirement, not just a best practice + if ($this->allowCredentials && in_array('*', $this->allowedOrigins, true)) { + throw new \RuntimeException( + 'CORS configuration error: Cannot use wildcard (*) origin with credentials enabled. ' . + 'This violates browser security policies. Either disable credentials or specify explicit origins.' + ); + } + + $this->log('CORS filter initialized', [ + 'allowed_origins' => $this->allowedOrigins, + 'allow_credentials' => $this->allowCredentials, + 'allowed_methods' => $this->allowedMethods, + ]); + } + + /** + * Check if a given origin is allowed to access this API + * + * Supports: + * - Exact matches: https://example.com + * - Wildcard origins: *.example.com + * - Scheme-less matching: example.com (matches http and https) + * - Universal wildcard: * + * + * @param string|null $origin The Origin header from the request + * @return bool True if origin is allowed, false otherwise + */ + protected function isOriginAllowed(?string $origin): bool + { + // Reject empty origins + if (empty($origin)) { + $this->log('Origin rejected: empty origin header'); + return false; + } + + // Validate origin format - must include scheme (http:// or https://) + // This prevents malformed origins from being accepted + if (!preg_match('#^https?://#i', $origin)) { + $this->log('Origin rejected: invalid format (missing scheme)', ['origin' => $origin]); + return false; + } + + // If wildcard present in configuration, allow any origin + if (in_array('*', $this->allowedOrigins, true)) { + $this->log('Origin allowed: wildcard match', ['origin' => $origin]); + return true; + } + + // Parse the host from the origin for wildcard matching + // Example: https://app.example.com:8080 → app.example.com + $originHost = parse_url($origin, PHP_URL_HOST) ?: $origin; + + foreach ($this->allowedOrigins as $allowed) { + if ($allowed === '') { + continue; + } + + // 1. Exact match (including scheme and port) + // Example: https://example.com matches https://example.com + if (strcasecmp($allowed, $origin) === 0) { + $this->log('Origin allowed: exact match', [ + 'origin' => $origin, + 'matched_rule' => $allowed + ]); + return true; + } + + // 2. Handle scheme-less and wildcard patterns + // If the allowed entry doesn't contain ://, it's either a host-only or wildcard pattern + if (strpos($allowed, '://') === false) { + + // 2a. Wildcard subdomain pattern: *.example.com + // Matches: app.example.com, api.example.com, dev.app.example.com + // Does NOT match: example.com (use explicit entry for root domain) + if (strpos($allowed, '*.') === 0) { + $allowedRoot = substr($allowed, 2); // Remove *. prefix + + // Check if origin host ends with the allowed root domain + if ($originHost === $allowedRoot || str_ends_with($originHost, '.' . $allowedRoot)) { + $this->log('Origin allowed: wildcard subdomain match', [ + 'origin' => $origin, + 'matched_rule' => $allowed, + 'origin_host' => $originHost + ]); + return true; + } + } + // 2b. Direct host match (scheme-less) + // Allows both http and https for the same host + // Example: example.com matches both http://example.com and https://example.com + else { + if (strcasecmp($allowed, $originHost) === 0) { + $this->log('Origin allowed: host match (scheme-less)', [ + 'origin' => $origin, + 'matched_rule' => $allowed, + 'origin_host' => $originHost + ]); + return true; + } + } + } + } + + // No match found - reject this origin + $this->log('Origin rejected: no matching rule', [ + 'origin' => $origin, + 'checked_rules' => $this->allowedOrigins + ]); + return false; + } + + /** + * Build the Access-Control-Allow-Origin header value + * + * Returns either: + * - '*' if wildcard is configured and credentials are disabled + * - The actual origin value if credentials are enabled or specific origins configured + * + * Note: When credentials are enabled, you MUST echo back the specific origin, + * browsers reject wildcard with credentials. + * + * @param string $origin The validated origin + * @return string The value for Access-Control-Allow-Origin header + */ + protected function buildAllowOriginHeader(string $origin): string + { + // If wildcard configured and credentials NOT required, can safely return '*' + // This allows any origin to access the resource + if (in_array('*', $this->allowedOrigins, true) && !$this->allowCredentials) { + return '*'; + } + + // Otherwise, must return the specific origin + // This is required when allow-credentials is true + return $origin; + } + + /** + * Add all CORS headers to the response + * + * This method is called for both preflight and actual requests + * to ensure consistent CORS headers across all responses. + * + * @param ResponseInterface $response The response object to add headers to + * @param RequestInterface $request The original request + * @param string $origin The validated origin + * @param bool $isPreflight Whether this is a preflight OPTIONS request + * @return void + */ + protected function addCorsHeaders( + ResponseInterface $response, + RequestInterface $request, + string $origin, + bool $isPreflight = false + ): void { + // CRITICAL: Vary header prevents caching issues + // Without this, a cached response for origin A might be served to origin B, + // causing CORS errors because the Access-Control-Allow-Origin won't match + $response->setHeader('Vary', 'Origin'); + + // Set the allowed origin + $allowOrigin = $this->buildAllowOriginHeader($origin); + $response->setHeader('Access-Control-Allow-Origin', $allowOrigin); + + // If credentials are allowed, set the header + // This allows cookies, authorization headers, and TLS client certificates + if ($this->allowCredentials) { + $response->setHeader('Access-Control-Allow-Credentials', 'true'); + } + + // Allowed HTTP methods + $response->setHeader('Access-Control-Allow-Methods', $this->allowedMethods); + + // Handle allowed headers + if ($isPreflight) { + // For preflight: respect what the browser is asking for + // The browser sends Access-Control-Request-Headers to ask permission + $requestedHeaders = $request->getHeaderLine('Access-Control-Request-Headers'); + $response->setHeader( + 'Access-Control-Allow-Headers', + $requestedHeaders ?: $this->allowedHeaders + ); + } else { + // For actual requests: use configured headers + // Access-Control-Request-Headers is only for preflight + $response->setHeader('Access-Control-Allow-Headers', $this->allowedHeaders); + } + + // Expose additional headers to the client (accessible via JavaScript) + // Without this, only simple headers are accessible: Cache-Control, Content-Language, + // Content-Type, Expires, Last-Modified, Pragma + if (!empty($this->exposeHeaders)) { + $response->setHeader('Access-Control-Expose-Headers', $this->exposeHeaders); + } + + // Cache duration for preflight responses + // Reduces preflight requests by allowing browser to cache the permissions + if ($this->maxAge > 0) { + $response->setHeader('Access-Control-Max-Age', (string) $this->maxAge); + } + } + + /** + * Execute before the controller + * + * Handles preflight OPTIONS requests by returning early with appropriate headers. + * For other requests, allows them to proceed to the controller. + * + * @param RequestInterface $request The request object + * @param mixed $arguments Optional arguments + * @return ResponseInterface|null Response for preflight, null for other requests + */ + public function before(RequestInterface $request, $arguments = null) + { + $origin = $request->getHeaderLine('Origin') ?: ''; + $method = strtoupper($request->getMethod()); + + // Handle preflight OPTIONS requests + // Preflight is sent by browsers before actual cross-origin requests + // to check if the actual request is safe to send + if ($method === 'OPTIONS') { + $this->log('Preflight request received', [ + 'origin' => $origin, + 'method' => $method, + 'uri' => (string) $request->getUri() + ]); + + // Validate origin - reject if not allowed + if (empty($origin) || !$this->isOriginAllowed($origin)) { + $this->log('Preflight rejected: origin not allowed', ['origin' => $origin]); + + // Return 403 Forbidden for rejected origins + // Some prefer 200 with no CORS headers, but 403 is more explicit + return Services::response() + ->setStatusCode(403) + ->setJSON(['error' => 'Origin not allowed']); + } + + // Origin is valid - build preflight response + $response = Services::response(); + $this->addCorsHeaders($response, $request, $origin, true); + + // 204 No Content is the standard response for successful preflight + // It indicates "permission granted, but no data to return" + $response->setStatusCode(204); + $response->setBody(''); + + $this->log('Preflight approved', [ + 'origin' => $origin, + 'allowed_methods' => $this->allowedMethods + ]); + + return $response; + } + + // For non-OPTIONS requests, don't return a response + // Let the request proceed to the controller + // CORS headers will be added in after() method + return null; + } + + /** + * Execute after the controller + * + * Adds CORS headers to the response for actual (non-preflight) requests. + * This ensures all API responses include proper CORS headers. + * + * @param RequestInterface $request The request object + * @param ResponseInterface $response The response object + * @param mixed $arguments Optional arguments + * @return void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + $origin = $request->getHeaderLine('Origin') ?: ''; + + // Only add CORS headers if origin is present and allowed + // No origin header means it's a same-origin request (no CORS needed) + if (empty($origin)) { + return; + } + + if (!$this->isOriginAllowed($origin)) { + $this->log('Response blocked: origin not allowed', [ + 'origin' => $origin, + 'uri' => (string) $request->getUri() + ]); + return; + } + + // Add CORS headers to the response + $this->addCorsHeaders($response, $request, $origin, false); + + $this->log('CORS headers added to response', [ + 'origin' => $origin, + 'status' => $response->getStatusCode() + ]); + } + + /** + * Log debug information if debug mode is enabled + * + * Logs to CodeIgniter's log system at 'info' level. + * Enable with CORS_DEBUG=true in .env file. + * + * @param string $message The log message + * @param array $context Additional context data + * @return void + */ + protected function log(string $message, array $context = []): void + { + if (!$this->debug) { + return; + } + + // $logger = Services::logger(); + $contextString = !empty($context) ? json_encode($context, JSON_UNESCAPED_SLASHES) : ''; + $this->myLogger->logme('error','[CORS] ' . $message . ($contextString ? ' | ' . $contextString : '')); + // $logger->info('[CORS] ' . $message . ($contextString ? ' | ' . $contextString : '')); + } +} \ No newline at end of file diff --git a/app/Models/InvoiceModel.php b/app/Models/InvoiceModel.php index 80a47e69..dcbd93fc 100644 --- a/app/Models/InvoiceModel.php +++ b/app/Models/InvoiceModel.php @@ -143,9 +143,12 @@ class InvoiceModel extends Model return $return_data; } - public function agentList() + public function agentList($params = []) { - return $this->db->table('partner_agent')->where('is_active', 1)->get()->getResultArray(); + if(isset($params['is_active'])){ + return $this->db->table('partner_agent')->where('is_active', $params['is_active'])->get()->getResultArray(); + } + return $this->db->table('partner_agent')->get()->getResultArray(); } public function utrSummary($invoice_id)