update webapp

This commit is contained in:
Purple
2026-01-17 20:33:16 +00:00
parent e73f1a65bf
commit c49bf2ed31
5 changed files with 1085 additions and 154 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -12,13 +12,15 @@ CREATE TABLE IF NOT EXISTS geofeed_entries (
region_code VARCHAR(10) DEFAULT NULL,
city VARCHAR(255) DEFAULT NULL,
postal_code VARCHAR(50) DEFAULT NULL,
client_short_name VARCHAR(100) DEFAULT NULL,
notes TEXT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_prefix (ip_prefix),
INDEX idx_country (country_code),
INDEX idx_region (region_code),
INDEX idx_city (city)
INDEX idx_city (city),
INDEX idx_client (client_short_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Audit log for tracking changes
@@ -42,6 +44,17 @@ CREATE TABLE IF NOT EXISTS geofeed_settings (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Client logos table for storing logo URLs per shortname
CREATE TABLE IF NOT EXISTS client_logos (
id INT AUTO_INCREMENT PRIMARY KEY,
short_name VARCHAR(100) NOT NULL,
logo_url VARCHAR(500) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_short_name (short_name),
INDEX idx_short_name (short_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert default settings
INSERT INTO geofeed_settings (setting_key, setting_value) VALUES
('bunny_cdn_storage_zone', ''),

View File

@@ -116,4 +116,4 @@ volumes:
networks:
geofeed-network:
driver: bridge
driver: bridge

View File

@@ -65,7 +65,27 @@ try {
case 'clear_all':
handleClearAll($db);
break;
case 'audit_log':
handleAuditLog($db);
break;
case 'logos_list':
handleLogosList($db);
break;
case 'logo_save':
handleLogoSave($db);
break;
case 'logo_delete':
handleLogoDelete($db);
break;
case 'shortnames_list':
handleShortnamesList($db);
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
@@ -90,12 +110,18 @@ function handleList($db) {
$params[':country'] = strtoupper($_GET['country']);
}
if (!empty($_GET['client'])) {
$where[] = 'client_short_name = :client';
$params[':client'] = $_GET['client'];
}
if (!empty($_GET['search'])) {
$where[] = '(ip_prefix LIKE :search OR city LIKE :search2 OR region_code LIKE :search3)';
$where[] = '(ip_prefix LIKE :search OR city LIKE :search2 OR region_code LIKE :search3 OR client_short_name LIKE :search4)';
$searchTerm = '%' . $_GET['search'] . '%';
$params[':search'] = $searchTerm;
$params[':search2'] = $searchTerm;
$params[':search3'] = $searchTerm;
$params[':search4'] = $searchTerm;
}
$whereClause = implode(' AND ', $where);
@@ -105,8 +131,13 @@ function handleList($db) {
$countStmt->execute($params);
$total = $countStmt->fetch()['total'];
// Get entries
$sql = "SELECT * FROM geofeed_entries WHERE $whereClause ORDER BY created_at DESC LIMIT :limit OFFSET :offset";
// Get entries - sorted by IP prefix using INET_ATON for proper IP sorting
$sql = "SELECT * FROM geofeed_entries WHERE $whereClause
ORDER BY
CASE WHEN ip_prefix LIKE '%:%' THEN 1 ELSE 0 END,
INET_ATON(SUBSTRING_INDEX(ip_prefix, '/', 1)),
ip_prefix
LIMIT :limit OFFSET :offset";
$stmt = $db->prepare($sql);
foreach ($params as $key => $value) {
@@ -195,8 +226,8 @@ function handleCreate($db) {
// Insert entry
$stmt = $db->prepare("
INSERT INTO geofeed_entries (ip_prefix, country_code, region_code, city, postal_code, notes)
VALUES (:ip_prefix, :country_code, :region_code, :city, :postal_code, :notes)
INSERT INTO geofeed_entries (ip_prefix, country_code, region_code, city, postal_code, client_short_name, notes)
VALUES (:ip_prefix, :country_code, :region_code, :city, :postal_code, :client_short_name, :notes)
");
$stmt->execute([
@@ -205,6 +236,7 @@ function handleCreate($db) {
':region_code' => $regionCode ?: null,
':city' => trim($input['city'] ?? '') ?: null,
':postal_code' => trim($input['postal_code'] ?? '') ?: null,
':client_short_name' => trim($input['client_short_name'] ?? '') ?: null,
':notes' => trim($input['notes'] ?? '') ?: null
]);
@@ -279,6 +311,7 @@ function handleUpdate($db) {
region_code = :region_code,
city = :city,
postal_code = :postal_code,
client_short_name = :client_short_name,
notes = :notes
WHERE id = :id
");
@@ -290,6 +323,7 @@ function handleUpdate($db) {
':region_code' => $regionCode ?: null,
':city' => trim($input['city'] ?? '') ?: null,
':postal_code' => trim($input['postal_code'] ?? '') ?: null,
':client_short_name' => trim($input['client_short_name'] ?? '') ?: null,
':notes' => trim($input['notes'] ?? '') ?: null
]);
@@ -747,7 +781,7 @@ function logAction($db, $entryId, $action, $oldValues, $newValues) {
INSERT INTO geofeed_audit_log (entry_id, action, old_values, new_values, changed_by)
VALUES (:entry_id, :action, :old_values, :new_values, :changed_by)
");
$stmt->execute([
':entry_id' => $entryId,
':action' => $action,
@@ -756,3 +790,147 @@ function logAction($db, $entryId, $action, $oldValues, $newValues) {
':changed_by' => $_SERVER['REMOTE_ADDR'] ?? 'system'
]);
}
/**
* Get audit log entries with pagination
*/
function handleAuditLog($db) {
$page = max(1, intval($_GET['page'] ?? 1));
$limit = min(100, max(10, intval($_GET['limit'] ?? 25)));
$offset = ($page - 1) * $limit;
// Get total count
$countStmt = $db->query("SELECT COUNT(*) as total FROM geofeed_audit_log");
$total = $countStmt->fetch()['total'];
// Get audit log entries with entry details
$sql = "SELECT
a.id,
a.entry_id,
a.action,
a.old_values,
a.new_values,
a.changed_at,
a.changed_by,
e.ip_prefix
FROM geofeed_audit_log a
LEFT JOIN geofeed_entries e ON a.entry_id = e.id
ORDER BY a.changed_at DESC
LIMIT :limit OFFSET :offset";
$stmt = $db->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$entries = $stmt->fetchAll();
// Parse JSON fields
foreach ($entries as &$entry) {
$entry['old_values'] = $entry['old_values'] ? json_decode($entry['old_values'], true) : null;
$entry['new_values'] = $entry['new_values'] ? json_decode($entry['new_values'], true) : null;
}
jsonResponse([
'success' => true,
'data' => $entries,
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit)
]
]);
}
/**
* List all client logos
*/
function handleLogosList($db) {
$stmt = $db->query("SELECT * FROM client_logos ORDER BY short_name");
$logos = $stmt->fetchAll();
jsonResponse(['success' => true, 'data' => $logos]);
}
/**
* Save (create or update) a client logo
*/
function handleLogoSave($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);
}
$shortName = trim($input['short_name'] ?? '');
$logoUrl = trim($input['logo_url'] ?? '');
if (empty($shortName)) {
jsonResponse(['error' => 'Short name is required'], 400);
}
if (empty($logoUrl) || !filter_var($logoUrl, FILTER_VALIDATE_URL)) {
jsonResponse(['error' => 'Valid logo URL is required'], 400);
}
$stmt = $db->prepare("
INSERT INTO client_logos (short_name, logo_url)
VALUES (:short_name, :logo_url)
ON DUPLICATE KEY UPDATE logo_url = VALUES(logo_url), updated_at = CURRENT_TIMESTAMP
");
$stmt->execute([
':short_name' => $shortName,
':logo_url' => $logoUrl
]);
jsonResponse(['success' => true, 'message' => 'Logo saved successfully']);
}
/**
* Delete a client logo
*/
function handleLogoDelete($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);
}
$shortName = trim($input['short_name'] ?? '');
if (empty($shortName)) {
jsonResponse(['error' => 'Short name is required'], 400);
}
$stmt = $db->prepare("DELETE FROM client_logos WHERE short_name = :short_name");
$stmt->execute([':short_name' => $shortName]);
jsonResponse(['success' => true, 'message' => 'Logo deleted successfully']);
}
/**
* Get list of unique client short names from entries
*/
function handleShortnamesList($db) {
$stmt = $db->query("
SELECT DISTINCT client_short_name
FROM geofeed_entries
WHERE client_short_name IS NOT NULL AND client_short_name != ''
ORDER BY client_short_name
");
$shortnames = $stmt->fetchAll(PDO::FETCH_COLUMN);
jsonResponse(['success' => true, 'data' => $shortnames]);
}

File diff suppressed because it is too large Load Diff