db_onedev = $this->load->database("onedev", true); $this->db_smartone = $this->load->database("onedev", true); // Daftar domain yang diizinkan mengakses API $allowedOrigins = [ 'https://devone.aplikasi.web.id', 'https://westerindo.com' // Tambahkan domain lain yang diizinkan di sini ]; // Ambil Origin dari request $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : ''; // Handle preflight request khusus if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { // Cek apakah origin ada dalam daftar yang diizinkan if (in_array($origin, $allowedOrigins)) { // Izinkan origin spesifik header("Access-Control-Allow-Origin: $origin"); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); // Ambil header yang diminta dari request preflight if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) { header('Access-Control-Allow-Headers: ' . $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']); } else { header('Access-Control-Allow-Headers: Content-Type, Authorization, authorization, Accept, X-Requested-With'); } header('Access-Control-Max-Age: 86400'); // Cache preflight selama 24 jam header('Content-Length: 0'); header('Content-Type: text/plain'); http_response_code(200); } else { // Origin tidak diizinkan - berikan respons 403 header('Content-Type: text/plain'); http_response_code(403); echo 'Origin not allowed'; } exit(0); // Penting untuk menghentikan eksekusi lebih lanjut } // Header untuk request normal (non-OPTIONS) if (in_array($origin, $allowedOrigins)) { header("Access-Control-Allow-Origin: $origin"); header('Access-Control-Allow-Methods: GET, POST'); header('Access-Control-Allow-Headers: Content-Type, Authorization, authorization, Accept'); } } /** * REST API handler untuk mendapatkan semua data harga * Implementasi dengan fitur keamanan yang baik * * @return void Output JSON response */ function get_branches() { // Pastikan hanya menerima request GET if ($_SERVER['REQUEST_METHOD'] !== 'GET') { $this->sendResponse(405, ['error' => 'Method Not Allowed', 'message' => 'Hanya metode GET yang diizinkan']); return; } // Validasi API key/token $apiKey = $this->getAuthorizationHeader(); if (!$this->validateApiKeyWithPermission($apiKey, 'branches:read')) { $this->sendResponse(401, ['error' => 'Unauthorized', 'message' => 'API key tidak valid']); return; } // --- QUERY DATABASE DENGAN AMAN --- try { $sql = "SELECT M_BranchCode as branch_code, M_BranchCodeLab as branch_code_lab, M_BranchName as branch_name, M_BranchAddress as branch_address FROM m_branch WHERE M_BranchIsActive = 'Y' ORDER BY M_BranchCode ASC"; $query = $this->db_onedev->query($sql); if(!$query){ $prm_log = ['GET_BRANCHES', 'branch/get_branches', $this->db_onedev->last_query()]; $log_error = $this->insert_log_error($sql, $prm_log); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan saat mengambil data']); return; } $rows = $query->result_array(); $response = [ 'status' => 'success', 'data' => $rows, 'meta' => [ 'total_records' => count($rows) ] ]; // --- 5. KIRIM RESPONSE --- $this->sendResponse(200, $response); } catch (PDOException $e) { // Log error tapi jangan tampilkan detail ke user error_log('Database error: ' . $e->getMessage()); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan saat mengambil data']); } catch (Exception $e) { error_log('API error: ' . $e->getMessage()); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan dalam pemrosesan']); } } function get_category() { // Pastikan hanya menerima request GET if ($_SERVER['REQUEST_METHOD'] !== 'GET') { $this->sendResponse(405, ['error' => 'Method Not Allowed', 'message' => 'Hanya metode GET yang diizinkan']); return; } // Validasi API key/token $apiKey = $this->getAuthorizationHeader(); if (!$this->validateApiKeyWithPermission($apiKey, 'prices:read')) { $this->sendResponse(401, ['error' => 'Unauthorized', 'message' => 'API key tidak valid']); return; } // --- QUERY DATABASE DENGAN AMAN --- try { $sql = "SELECT Nat_SubGroupSasCode as category, Nat_SubGroupFormalName as category_name FROM nat_subgroup WHERE Nat_SubGroupIsActive = 'Y' ORDER BY Nat_SubGroupSasCode ASC"; $query = $this->db_onedev->query($sql); if(!$query){ $prm_log = ['GET_CATEGORY', 'price/get_category', $this->db_onedev->last_query()]; $log_error = $this->insert_log_error($sql, $prm_log); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan saat mengambil data']); return; } $rows = $query->result_array(); $response = [ 'status' => 'success', 'data' => $rows, 'meta' => [ 'total_records' => count($rows) ] ]; // --- 5. KIRIM RESPONSE --- $this->sendResponse(200, $response); } catch (PDOException $e) { // Log error tapi jangan tampilkan detail ke user error_log('Database error: ' . $e->getMessage()); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan saat mengambil data']); } catch (Exception $e) { error_log('API error: ' . $e->getMessage()); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan dalam pemrosesan']); } } function get_single_price() { // --- 1. VALIDASI REQUEST DAN KEAMANAN --- // Pastikan hanya menerima request GET if ($_SERVER['REQUEST_METHOD'] !== 'GET') { $this->sendResponse(405, ['error' => 'Method Not Allowed', 'message' => 'Hanya metode GET yang diizinkan']); return; } // Validasi API key/token $apiKey = $this->getAuthorizationHeader(); if (!$this->validateApiKeyWithPermission($apiKey, 'prices:read')) { $this->sendResponse(401, ['error' => 'Unauthorized', 'message' => 'API key tidak valid']); return; } // --- 2. PARAMETER FILTERING & PAGINATION --- // Ambil dan validasi parameter filtering (jika ada) $filters = []; $allowedFilters = ['search','category']; foreach ($allowedFilters as $filter) { if (isset($_GET[$filter])) { // Sanitasi input $filters[$filter] = $this->sanitizeInput($_GET[$filter]); } } //parameter $search = isset($filters['search']) ? $filters['search'] : ''; $category = isset($filters['category']) ? $filters['category'] : ''; // Pagination $page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1; $limit = isset($_GET['limit']) ? min(100, max(1, intval($_GET['limit']))) : 20; // Default 20, max 100 $offset = ($page - 1) * $limit; // --- 3. QUERY DATABASE DENGAN AMAN --- try { $params = []; $params[] = '%'.$search.'%'; $filter_category = ''; if($category != ''){ $filter_category = " AND Nat_SubGroupSasCode = ?"; $params[] = $category; } $sql = "SELECT COUNT(*) as total FROM `ss_price_mou` JOIN `t_test` test ON test.T_TestID = ss_price_mou.T_TestID AND test.T_TestName LIKE ? JOIN `nat_test` ON nat_test.Nat_TestID = test.T_TestNat_TestID JOIN `nat_subgroup` ON `Nat_TestNat_SubGroupID` = `Nat_SubGroupID` $filter_category JOIN mgm_mcu ON mgm_mcu.Mgm_McuT_PriceHeaderID = ss_price_mou.T_PriceT_PriceHeaderID JOIN config_website ON config_website.configWebsiteMgm_McuID = mgm_mcu.Mgm_McuID WHERE ss_price_mou.is_packet = 'N'"; $query = $this->db_onedev->query($sql, $params); if(!$query){ $prm_log = ['GET_PRICE_SINGLE', 'price/get_single_price', $this->db_onedev->last_query()]; $log_error = $this->insert_log_error($sql, $prm_log); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan saat mengambil data']); return; } $totalCount = $query->row_array()['total']; $totalPages = ceil($totalCount / $limit); $params[] = $offset; $params[] = $limit; $sql = "SELECT Mgm_McuNumber as project_number, ss_price_mou.Ss_PriceMouID as x_id, test.T_TestID as test_id, nat_test.Nat_TestID as test_nat_id, test.T_TestName as test_name, test.T_TestSasCode as test_sas_code, Nat_SubGroupName as category, ss_price_mou.T_TestRequirement as test_requirement, ss_price_mou.T_PriceAmount as price, ss_price_mou.T_PriceDisc as disc, ss_price_mou.T_PriceDiscRp as disc_rp, ss_price_mou.T_PriceSubTotal as subtotal, ss_price_mou.T_PriceTotal as total, ss_price_mou.px_type as px_type, ss_price_mou.nat_test as nat_tests, ss_price_mou.child_test as child_test, ss_price_mou.T_PriceT_PriceHeaderID as price_header_id, IFNULL(nat_test_desc.Nat_TestDescNote, '') as test_desc FROM ss_price_mou JOIN t_test test ON test.T_TestID = ss_price_mou.T_TestID AND test.T_TestName LIKE ? JOIN nat_test ON nat_test.Nat_TestID = test.T_TestNat_TestID JOIN nat_subgroup ON Nat_TestNat_SubGroupID = Nat_SubGroupID $filter_category JOIN mgm_mcu ON mgm_mcu.Mgm_McuT_PriceHeaderID = ss_price_mou.T_PriceT_PriceHeaderID JOIN config_website ON config_website.configWebsiteMgm_McuID = mgm_mcu.Mgm_McuID LEFT JOIN nat_test_desc ON nat_test_desc.Nat_TestDescNat_TestID = nat_test.Nat_TestID WHERE ss_price_mou.is_packet = 'N' ORDER BY ss_price_mou.T_TestName ASC LIMIT ?, ?"; $query = $this->db_onedev->query($sql, $params); if(!$query){ $prm_log = ['GET_PRICE_SINGLE', 'price/get_single_price', $this->db_onedev->last_query()]; $log_error = $this->insert_log_error($sql, $prm_log); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan saat mengambil data']); return; } $rows = $query->result_array(); if(count($rows) > 0){ foreach($rows as $key => $row){ $rows[$key]['child_test'] = json_decode($row['child_test']); } } $response = [ 'status' => 'success', 'data' => $rows, 'meta' => [ 'page' => $page, 'limit' => $limit, 'total_records' => $totalCount, 'total_pages' => $totalPages ] ]; // --- 5. KIRIM RESPONSE --- $this->sendResponse(200, $response); } catch (PDOException $e) { // Log error tapi jangan tampilkan detail ke user error_log('Database error: ' . $e->getMessage()); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan saat mengambil data']); } catch (Exception $e) { error_log('API error: ' . $e->getMessage()); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan dalam pemrosesan']); } } function get_packet_price() { // --- 1. VALIDASI REQUEST DAN KEAMANAN --- // Pastikan hanya menerima request GET if ($_SERVER['REQUEST_METHOD'] !== 'GET') { $this->sendResponse(405, ['error' => 'Method Not Allowed', 'message' => 'Hanya metode GET yang diizinkan']); return; } // Validasi API key/token $apiKey = $this->getAuthorizationHeader(); if (!$this->validateApiKeyWithPermission($apiKey, 'prices:read')) { $this->sendResponse(401, ['error' => 'Unauthorized', 'message' => 'API key tidak valid']); return; } // --- 2. PARAMETER FILTERING & PAGINATION --- // Ambil dan validasi parameter filtering (jika ada) $filters = []; $allowedFilters = ['search']; foreach ($allowedFilters as $filter) { if (isset($_GET[$filter])) { // Sanitasi input $filters[$filter] = $this->sanitizeInput($_GET[$filter]); } } //parameter $search = isset($filters['search']) ? $filters['search'] : ''; // Pagination $page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1; $limit = isset($_GET['limit']) ? min(100, max(1, intval($_GET['limit']))) : 20; // Default 20, max 100 $offset = ($page - 1) * $limit; // --- 3. QUERY DATABASE DENGAN AMAN --- try { $params = []; $params[] = '%'.$search.'%'; $sql = "SELECT COUNT(*) as total FROM `ss_price_mou` JOIN `t_test` test ON test.T_TestID = ss_price_mou.T_TestID AND test.T_TestName LIKE ? JOIN `nat_test` ON nat_test.Nat_TestID = test.T_TestNat_TestID JOIN `nat_subgroup` ON `Nat_TestNat_SubGroupID` = `Nat_SubGroupID` JOIN mgm_mcu ON mgm_mcu.Mgm_McuT_PriceHeaderID = ss_price_mou.T_PriceT_PriceHeaderID JOIN config_website ON config_website.configWebsiteMgm_McuID = mgm_mcu.Mgm_McuID WHERE ss_price_mou.is_packet = 'Y'"; $query = $this->db_onedev->query($sql, $params); if(!$query){ $prm_log = ['GET_PRICE_PACKET', 'price/get_packet_price', $this->db_onedev->last_query()]; $log_error = $this->insert_log_error($sql, $prm_log); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan saat mengambil data']); return; } $totalCount = $query->row_array()['total']; $totalPages = ceil($totalCount / $limit); $params[] = $offset; $params[] = $limit; $sql = "SELECT Mgm_McuNumber as project_number, ss_price_mou.Ss_PriceMouID as x_id, t_packet.T_PacketID as test_id, 0 as test_nat_id, ss_price_mou.T_TestName as test_name, t_packet.T_PacketSasCode as test_sas_code, 'packet' as category, ss_price_mou.T_TestRequirement as test_requirement, ss_price_mou.T_PriceAmount as price, ss_price_mou.T_PriceDisc as disc, ss_price_mou.T_PriceDiscRp as disc_rp, ss_price_mou.T_PriceSubTotal as subtotal, ss_price_mou.T_PriceTotal as total, ss_price_mou.px_type as px_type, ss_price_mou.nat_test as nat_tests, ss_price_mou.child_test as child_test, ss_price_mou.T_PriceT_PriceHeaderID as price_header_id, IFNULL(T_PacketDescNote, '') as test_desc FROM ss_price_mou JOIN t_packet ON t_packet.T_PacketID = ss_price_mou.T_TestID AND t_packet.T_PacketName LIKE ? JOIN mgm_mcu ON mgm_mcu.Mgm_McuT_PriceHeaderID = ss_price_mou.T_PriceT_PriceHeaderID JOIN config_website ON config_website.configWebsiteMgm_McuID = mgm_mcu.Mgm_McuID LEFT JOIN t_packet_desc_map ON T_PacketDescMapT_PacketID = ss_price_mou.T_TestID AND T_PacketDescMapIsActive = 'Y' LEFT JOIN t_packet_desc ON T_PacketDescMapT_PacketDescID = t_packet_desc.T_PacketDescID WHERE ss_price_mou.is_packet = 'Y' ORDER BY ss_price_mou.T_TestName ASC LIMIT ?, ?"; $query = $this->db_onedev->query($sql, $params); if(!$query){ $prm_log = ['GET_PRICE_PACKET', 'price/get_packet_price', $this->db_onedev->last_query()]; $log_error = $this->insert_log_error($sql, $prm_log); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan saat mengambil data']); return; } $rows = $query->result_array(); if(count($rows) > 0){ foreach($rows as $key => $row){ $rows[$key]['child_test'] = json_decode($row['child_test']); } } $response = [ 'status' => 'success', 'data' => $rows, 'meta' => [ 'page' => $page, 'limit' => $limit, 'total_records' => $totalCount, 'total_pages' => $totalPages ] ]; // --- 5. KIRIM RESPONSE --- $this->sendResponse(200, $response); } catch (PDOException $e) { // Log error tapi jangan tampilkan detail ke user error_log('Database error: ' . $e->getMessage()); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan saat mengambil data']); } catch (Exception $e) { error_log('API error: ' . $e->getMessage()); $this->sendResponse(500, ['error' => 'Internal Server Error', 'message' => 'Terjadi kesalahan dalam pemrosesan']); } } function insert_log_error($sql, $params){ $sql = "INSERT INTO error_log_website( ErrorLogWebsiteCode, ErrorLogWebsiteFnName, ErrorLogWebsiteDescription, ErrorLogWebsiteCreated ) VALUES( ?,?,?,NOW() )"; $query = $this->db_onedev->query($sql, $params); if(!$query){ return false; } return true; } /** * Mendapatkan API key dari header Authorization * * @return string|null API key atau null jika tidak ada */ private function getAuthorizationHeader() { $headers = null; if (isset($_SERVER['Authorization'])) { $headers = trim($_SERVER['Authorization']); } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { $headers = trim($_SERVER['HTTP_AUTHORIZATION']); } else if (function_exists('apache_request_headers')) { $requestHeaders = apache_request_headers(); $requestHeaders = array_combine( array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders) ); if (isset($requestHeaders['Authorization'])) { $headers = trim($requestHeaders['Authorization']); } } // Format header biasanya "Bearer {token}" if (!empty($headers)) { if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) { return $matches[1]; } } return null; } /** * Validasi API key * * @param string $apiKey API key untuk divalidasi * @return bool True jika valid, false jika tidak */ private function validateApiKey($apiKey) { if (empty($apiKey)) { return false; } // Contoh implementasi validasi: // 1. Cek token di database // 2. Validasi JWT token // 3. Cek api key statis (hanya untuk contoh sederhana) try { $sql = "SELECT * FROM api_keys WHERE api_key = ? AND is_active = 1 AND expired_at > NOW()"; $prm = [$apiKey]; $query = $this->db_onedev->query($sql, $prm); $data = $query->row_array(); if ($data) { return true; } return false; } catch (Exception $e) { error_log('API key validation error: ' . $e->getMessage()); return false; } } /** * Sanitasi input untuk mencegah SQL injection dan XSS * * @param string $input Input yang akan disanitasi * @return string Input yang sudah disanitasi */ private function sanitizeInput($input) { if (is_string($input)) { // Hapus karakter tidak perlu $input = trim($input); // Hilangkan HTML tags $input = strip_tags($input); // Konversi special characters ke HTML entities $input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8'); return $input; } return $input; } /** * Kirim HTTP response dengan JSON * * @param int $statusCode HTTP status code * @param array $data Data yang akan dikirim sebagai JSON * @return void */ private function sendResponse($statusCode, $data) { http_response_code($statusCode); header('Content-Type: application/json; charset=utf-8'); // Tambahkan header keamanan header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header('X-XSS-Protection: 1; mode=block'); header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); header('Content-Security-Policy: default-src \'self\''); // Set cache control untuk kontrol caching header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); // Konversi data ke JSON dan kirim echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); exit; } // Fungsi untuk menghasilkan API key baru function generateApiKey($userId, $name, $description, $rateLimit = 100, $expiryDays = 365) { try { $permissions = 'prices:read'; // Generate random token $apiKey = bin2hex(random_bytes(32)); // 64 karakter hex // Set tanggal kadaluarsa (jika ada) $expiredAt = $expiryDays ? date('Y-m-d H:i:s', strtotime("+{$expiryDays} days")) : null; // Convert permissions to JSON if it's an array if (is_array($permissions)) { $permissions = json_encode($permissions); } // Insert ke database $sql = "INSERT INTO api_keys ( user_id, api_key, name, description, permissions, rate_limit, is_active, created_by, expired_at ) VALUES ( ?, ?, ?, ?, ?, ?, 1, ?, ? )"; $prm = [$userId, $apiKey, $name, $description, $permissions, $rateLimit, $userId, $expiredAt]; $query = $this->db_onedev->query($sql, $prm); $data = $query->row_array(); if ($data) { return [ 'id' => $db->lastInsertId(), 'api_key' => $apiKey, 'expired_at' => $expiredAt ]; } return false; } catch (Exception $e) { error_log('Error generating API key: ' . $e->getMessage()); return false; } } // Fungsi untuk menonaktifkan API key function deactivateApiKey($apiKeyId, $userId) { try { $sql = "UPDATE api_keys SET is_active = 0, updated_at = NOW(), updated_by = ? WHERE id = ? AND is_deleted = 0"; $prm = [$userId, $apiKeyId]; $query = $this->db_onedev->query($sql, $prm); $data = $query->row_array(); if ($data) { return true; } return false; } catch (Exception $e) { error_log('Error deactivating API key: ' . $e->getMessage()); return false; } } // Fungsi untuk menghapus API key (soft delete) function deleteApiKey($apiKeyId, $userId) { try { $sql = "UPDATE api_keys SET is_deleted = 1, deleted_at = NOW(), deleted_by = ?, is_active = 0 WHERE id = ? AND is_deleted = 0"; $prm = [$userId, $apiKeyId]; $query = $this->db_onedev->query($sql, $prm); $data = $query->row_array(); if ($data) { return true; } return false; } catch (Exception $e) { error_log('Error deleting API key: ' . $e->getMessage()); return false; } } // Fungsi untuk memvalidasi API key dan cek izin function validateApiKeyWithPermission($apiKey, $requiredPermission) { try { $sql = "SELECT id, user_id, permissions, rate_limit, last_used_at FROM api_keys WHERE api_key = ? AND is_active = 1 AND is_deleted = 0 AND (expired_at IS NULL OR expired_at > NOW())"; $prm = [$apiKey]; $query = $this->db_onedev->query($sql, $prm); $apiKeyData = $query->row_array(); if (!$apiKeyData) { return false; // API key tidak valid atau tidak aktif } // Update last_used_at $sql = "UPDATE api_keys SET last_used_at = NOW() WHERE id = ? AND is_deleted = 0"; $prm = [$apiKeyData['id']]; $query = $this->db_onedev->query($sql, $prm); if(!$query){ $prm_log = ['UPDATE_API_KEY_LAST_USED', 'price/validateApiKeyWithPermission', $this->db_onedev->last_query()]; $log_error = $this->insert_log_error($sql, $prm_log); return false; } // Cek rate limiting (implementasi sederhana) if ($apiKeyData['last_used_at']) { $lastUsed = new DateTime($apiKeyData['last_used_at']); $now = new DateTime(); $diff = $now->getTimestamp() - $lastUsed->getTimestamp(); // Jika terakhir digunakan kurang dari 1 detik yang lalu, dan sudah melebihi rate limit // Ini implementasi sederhana, idealnya gunakan sistem rate limiting yang lebih kompleks if ($diff < 1 && $this->countRecentRequests($apiKeyData['id']) >= $apiKeyData['rate_limit']) { return ['error' => 'rate_limit_exceeded']; } } // Cek permissions jika ada if ($requiredPermission) { $permissions = json_decode($apiKeyData['permissions'], true); // Format permission: {"resource": "action1,action2"} // contoh: {"prices": "read,write"} list($resource, $action) = explode(':', $requiredPermission); if (!isset($permissions[$resource]) || !in_array($action, explode(',', $permissions[$resource]))) { return ['error' => 'permission_denied']; } } return $apiKeyData; } catch (Exception $e) { error_log('API key validation error: ' . $e->getMessage()); return false; } } }