add styling
This commit is contained in:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
|
*.md
|
||||||
|
LICENSE
|
||||||
|
docker-compose.yml
|
||||||
|
Dockerfile
|
||||||
|
import-geofeed.sh
|
||||||
|
n8n/
|
||||||
17
.env.example
Normal file
17
.env.example
Normal file
@@ -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
|
||||||
301
webapp/api.php
301
webapp/api.php
@@ -54,6 +54,18 @@ try {
|
|||||||
handleSearch($db);
|
handleSearch($db);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'import':
|
||||||
|
handleImport($db);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'import_url':
|
||||||
|
handleImportUrl($db);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'clear_all':
|
||||||
|
handleClearAll($db);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
jsonResponse(['error' => 'Invalid action'], 400);
|
jsonResponse(['error' => 'Invalid action'], 400);
|
||||||
}
|
}
|
||||||
@@ -438,6 +450,295 @@ function handleSearch($db) {
|
|||||||
jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]);
|
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
|
* Log action to audit table
|
||||||
*/
|
*/
|
||||||
|
|||||||
1012
webapp/index.php
1012
webapp/index.php
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user