diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..054b1f5 Binary files /dev/null and b/.DS_Store differ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ff1e9f5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.gitignore +.env +.env.example +*.md +LICENSE +docker-compose.yml +Dockerfile +import-geofeed.sh +n8n/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a3a6950 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Geofeed Manager Environment Configuration +# Copy this file to .env and update the values + +# Database Configuration +DB_ROOT_PASSWORD=change_me_root_password +DB_NAME=geofeed_manager +DB_USER=geofeed +DB_PASSWORD=change_me_user_password + +# Port Configuration +DB_PORT=3306 +WEB_PORT=8080 +PMA_PORT=8081 + +# BunnyCDN Configuration (for n8n workflow) +BUNNY_STORAGE_ZONE=your-storage-zone +BUNNY_API_KEY=your-api-key diff --git a/webapp/api.php b/webapp/api.php index 1ba14b9..e224dbe 100644 --- a/webapp/api.php +++ b/webapp/api.php @@ -54,6 +54,18 @@ try { handleSearch($db); break; + case 'import': + handleImport($db); + break; + + case 'import_url': + handleImportUrl($db); + break; + + case 'clear_all': + handleClearAll($db); + break; + default: jsonResponse(['error' => 'Invalid action'], 400); } @@ -438,6 +450,295 @@ function handleSearch($db) { jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]); } +/** + * Import entries from parsed CSV data + */ +function handleImport($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $input = json_decode(file_get_contents('php://input'), true); + + // Validate CSRF + if (!validateCSRFToken($input['csrf_token'] ?? '')) { + jsonResponse(['error' => 'Invalid CSRF token'], 403); + } + + $entries = $input['entries'] ?? []; + + if (empty($entries)) { + jsonResponse(['error' => 'No entries provided'], 400); + } + + $inserted = 0; + $updated = 0; + $failed = 0; + $errors = []; + + $stmt = $db->prepare(" + INSERT INTO geofeed_entries (ip_prefix, country_code, region_code, city, postal_code) + VALUES (:ip_prefix, :country_code, :region_code, :city, :postal_code) + ON DUPLICATE KEY UPDATE + country_code = VALUES(country_code), + region_code = VALUES(region_code), + city = VALUES(city), + postal_code = VALUES(postal_code), + updated_at = CURRENT_TIMESTAMP + "); + + $db->beginTransaction(); + + try { + foreach ($entries as $entry) { + $ipPrefix = trim($entry['ip_prefix'] ?? ''); + + if (empty($ipPrefix) || !isValidIpPrefix($ipPrefix)) { + $failed++; + continue; + } + + $countryCode = strtoupper(trim($entry['country_code'] ?? '')); + if (!empty($countryCode) && !isValidCountryCode($countryCode)) { + $countryCode = null; + } + + $regionCode = strtoupper(trim($entry['region_code'] ?? '')); + if (!empty($regionCode) && !isValidRegionCode($regionCode)) { + $regionCode = null; + } + + try { + $stmt->execute([ + ':ip_prefix' => $ipPrefix, + ':country_code' => $countryCode ?: null, + ':region_code' => $regionCode ?: null, + ':city' => trim($entry['city'] ?? '') ?: null, + ':postal_code' => trim($entry['postal_code'] ?? '') ?: null + ]); + + if ($stmt->rowCount() === 1) { + $inserted++; + } elseif ($stmt->rowCount() === 2) { + $updated++; + } + } catch (PDOException $e) { + $failed++; + } + } + + $db->commit(); + + // Log the import + logAction($db, null, 'INSERT', null, [ + 'type' => 'bulk_import', + 'inserted' => $inserted, + 'updated' => $updated, + 'failed' => $failed + ]); + + jsonResponse([ + 'success' => true, + 'inserted' => $inserted, + 'updated' => $updated, + 'failed' => $failed + ]); + + } catch (Exception $e) { + $db->rollBack(); + jsonResponse(['error' => 'Import failed: ' . $e->getMessage()], 500); + } +} + +/** + * Import entries from a remote URL + */ +function handleImportUrl($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $input = json_decode(file_get_contents('php://input'), true); + + // Validate CSRF + if (!validateCSRFToken($input['csrf_token'] ?? '')) { + jsonResponse(['error' => 'Invalid CSRF token'], 403); + } + + $url = trim($input['url'] ?? ''); + + if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) { + jsonResponse(['error' => 'Invalid URL'], 400); + } + + // Fetch the CSV + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_USERAGENT => 'Geofeed-Manager/1.0' + ]); + + $csvData = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error || $httpCode !== 200) { + jsonResponse(['error' => 'Failed to fetch URL: ' . ($error ?: "HTTP $httpCode")], 400); + } + + if (empty($csvData)) { + jsonResponse(['error' => 'Empty response from URL'], 400); + } + + // Parse CSV + $lines = explode("\n", $csvData); + $entries = []; + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line) || strpos($line, '#') === 0) { + continue; + } + + $parts = str_getcsv($line); + if (count($parts) < 1 || empty(trim($parts[0]))) { + continue; + } + + $entries[] = [ + 'ip_prefix' => trim($parts[0] ?? ''), + 'country_code' => strtoupper(trim($parts[1] ?? '')), + 'region_code' => strtoupper(trim($parts[2] ?? '')), + 'city' => trim($parts[3] ?? ''), + 'postal_code' => trim($parts[4] ?? '') + ]; + } + + if (empty($entries)) { + jsonResponse(['error' => 'No valid entries found in CSV'], 400); + } + + // Import entries + $inserted = 0; + $updated = 0; + $failed = 0; + + $stmt = $db->prepare(" + INSERT INTO geofeed_entries (ip_prefix, country_code, region_code, city, postal_code) + VALUES (:ip_prefix, :country_code, :region_code, :city, :postal_code) + ON DUPLICATE KEY UPDATE + country_code = VALUES(country_code), + region_code = VALUES(region_code), + city = VALUES(city), + postal_code = VALUES(postal_code), + updated_at = CURRENT_TIMESTAMP + "); + + $db->beginTransaction(); + + try { + foreach ($entries as $entry) { + $ipPrefix = $entry['ip_prefix']; + + if (!isValidIpPrefix($ipPrefix)) { + $failed++; + continue; + } + + $countryCode = $entry['country_code']; + if (!empty($countryCode) && !isValidCountryCode($countryCode)) { + $countryCode = null; + } + + $regionCode = $entry['region_code']; + if (!empty($regionCode) && !isValidRegionCode($regionCode)) { + $regionCode = null; + } + + try { + $stmt->execute([ + ':ip_prefix' => $ipPrefix, + ':country_code' => $countryCode ?: null, + ':region_code' => $regionCode ?: null, + ':city' => $entry['city'] ?: null, + ':postal_code' => $entry['postal_code'] ?: null + ]); + + if ($stmt->rowCount() === 1) { + $inserted++; + } elseif ($stmt->rowCount() === 2) { + $updated++; + } + } catch (PDOException $e) { + $failed++; + } + } + + $db->commit(); + + // Log the import + logAction($db, null, 'INSERT', null, [ + 'type' => 'url_import', + 'url' => $url, + 'inserted' => $inserted, + 'updated' => $updated, + 'failed' => $failed + ]); + + jsonResponse([ + 'success' => true, + 'inserted' => $inserted, + 'updated' => $updated, + 'failed' => $failed + ]); + + } catch (Exception $e) { + $db->rollBack(); + jsonResponse(['error' => 'Import failed: ' . $e->getMessage()], 500); + } +} + +/** + * Clear all entries + */ +function handleClearAll($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $input = json_decode(file_get_contents('php://input'), true); + + // Validate CSRF + if (!validateCSRFToken($input['csrf_token'] ?? '')) { + jsonResponse(['error' => 'Invalid CSRF token'], 403); + } + + try { + // Get count before deletion + $countStmt = $db->query("SELECT COUNT(*) as count FROM geofeed_entries"); + $count = $countStmt->fetch()['count']; + + // Delete all entries + $db->exec("DELETE FROM geofeed_entries"); + + // Reset auto increment + $db->exec("ALTER TABLE geofeed_entries AUTO_INCREMENT = 1"); + + // Log the action + logAction($db, null, 'DELETE', ['count' => $count], ['type' => 'clear_all']); + + jsonResponse(['success' => true, 'deleted' => $count]); + + } catch (Exception $e) { + jsonResponse(['error' => 'Failed to clear entries: ' . $e->getMessage()], 500); + } +} + /** * Log action to audit table */ diff --git a/webapp/index.php b/webapp/index.php index ff5c6ee..cbfcf41 100644 --- a/webapp/index.php +++ b/webapp/index.php @@ -3,35 +3,50 @@
-