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 @@ - Geofeed Manager + Geofeed Manager | Purple Computing - + @@ -691,16 +1012,19 @@
- - + +
+ + +
+ +
+
+
Total Entries
+
-
+
+
+
IPv4 Prefixes
+
-
+
+
+
IPv6 Prefixes
+
-
+
+
+
Countries
+
-
+
+
+ + +
+ +
+ + +
+
+

Geofeed Entries

+
+ +
+
+
+
+
+
+
- -
-
-

Geofeed Entries

-
- + +
+
+

Import Geofeed Data

+

Import geofeed entries from a CSV file or a remote URL. The data should follow RFC 8805 format: ip_prefix,country_code,region_code,city,postal_code

+ +
+ +
+
+ + + + + +
+

Upload CSV File

+

Upload a geofeed CSV file from your computer

+ +
+ + +
+
+ +
+
+
+
+
Processing...
+
+ + + +
+
+ + + + Import Complete +
+
+
+
+ + +
+
+ + + + + +
+

Import from URL

+

Fetch and import a geofeed from a remote URL

+ +
+ +
+ +
+
+
+
+
Fetching...
+
+ + + +
+
+ + + + Import Complete +
+
+
+
-
-
-
-
+ +
+

Danger Zone

+

Irreversible actions - please proceed with caution.

+ +
+ + + + +
@@ -850,14 +1331,29 @@ let totalPages = 1; let searchTimeout = null; let deleteEntryId = null; + let selectedFile = null; const csrfToken = ''; // Initialize document.addEventListener('DOMContentLoaded', () => { loadEntries(); loadStats(); + + // Enable clear all button when "DELETE" is typed + document.getElementById('confirmClearInput').addEventListener('input', (e) => { + document.getElementById('confirmClearBtn').disabled = e.target.value !== 'DELETE'; + }); }); + // Tab switching + function switchTab(tab) { + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); + + document.querySelector(`.tab[onclick="switchTab('${tab}')"]`).classList.add('active'); + document.getElementById(`tab-${tab}`).classList.add('active'); + } + // API Helper async function api(action, params = {}, method = 'GET', body = null) { const url = new URL('api.php', window.location.href); @@ -902,14 +1398,14 @@ document.getElementById('tableContent').innerHTML = `
- +

No entries found

-

Get started by adding your first geofeed entry.

+

Get started by adding your first geofeed entry or import from the Advanced tab.

`; @@ -938,11 +1434,11 @@ ${getFlagEmoji(entry.country_code)} ${escapeHtml(entry.country_code)} - ` : ''} + ` : ''} - ${entry.region_code ? escapeHtml(entry.region_code) : ''} - ${entry.city ? escapeHtml(entry.city) : ''} - ${entry.postal_code ? escapeHtml(entry.postal_code) : ''} + ${entry.region_code ? escapeHtml(entry.region_code) : ''} + ${entry.city ? escapeHtml(entry.city) : ''} + ${entry.postal_code ? escapeHtml(entry.postal_code) : ''}