diff --git a/database/schema.sql b/database/schema.sql index 033f588..9dfd932 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -152,7 +152,8 @@ INSERT INTO geofeed_settings (setting_key, setting_value) VALUES ('aws_secret_access_key', ''), ('aws_region', 'us-east-1'), ('aws_hosted_zones', ''), -('whitelabel_company_name', 'Geofeed Manager'), +('whitelabel_app_name', 'ISP IP Manager'), +('whitelabel_company_name', ''), ('whitelabel_icon_url', ''), ('whitelabel_favicon_url', ''), ('whitelabel_default_import_url', '') diff --git a/webapp/api.php b/webapp/api.php index 8087351..6257553 100644 --- a/webapp/api.php +++ b/webapp/api.php @@ -2974,7 +2974,8 @@ function handleWhitelabelGet($db) { jsonResponse([ 'success' => true, 'settings' => [ - 'company_name' => getSetting($db, 'whitelabel_company_name', 'Geofeed Manager'), + 'app_name' => getSetting($db, 'whitelabel_app_name', 'ISP IP Manager'), + 'company_name' => getSetting($db, 'whitelabel_company_name', ''), 'icon_url' => getSetting($db, 'whitelabel_icon_url', ''), 'favicon_url' => getSetting($db, 'whitelabel_favicon_url', ''), 'default_import_url' => getSetting($db, 'whitelabel_default_import_url', '') @@ -3002,7 +3003,8 @@ function handleWhitelabelSave($db) { try { $settings = [ - 'whitelabel_company_name' => $input['company_name'] ?? 'Geofeed Manager', + 'whitelabel_app_name' => $input['app_name'] ?? 'ISP IP Manager', + 'whitelabel_company_name' => $input['company_name'] ?? '', 'whitelabel_icon_url' => $input['icon_url'] ?? '', 'whitelabel_favicon_url' => $input['favicon_url'] ?? '', 'whitelabel_default_import_url' => $input['default_import_url'] ?? '' diff --git a/webapp/index.php b/webapp/index.php index b636592..6b2af51 100644 --- a/webapp/index.php +++ b/webapp/index.php @@ -32,7 +32,7 @@ if (function_exists('requireAuth')) { - Geofeed Manager | Purple Computing + ISP IP Manager @@ -1599,8 +1599,8 @@ if (function_exists('requireAuth')) {
- Geofeed Manager - PURPLE COMPUTING + ISP IP Manager +
@@ -1620,6 +1620,24 @@ if (function_exists('requireAuth')) {
+ + + +
@@ -1751,147 +1783,6 @@ if (function_exists('requireAuth')) {

Advanced Settings

- -
-

- - - - - Audit Log -

-

View all changes made to geofeed entries including creates, updates, and deletes.

- -
-
-
-
-
-
- -
-
- - -
-

- - - - - n8n Webhook Integration -

-

Configure webhooks to notify n8n when geofeed data changes. Updates are debounced to batch multiple changes and reduce API calls.

- -
- -
- -
- - -
The n8n webhook URL to receive notifications
-
- -
- - -
Wait this many minutes after the last change before triggering the webhook (1-60 minutes)
-
- -
- - - -
- - -
-
-

Webhook Queue

-
- -
-
-
-
-
-
-
-
-
- - -
-

- - - - - - IP Registry Integration -

-

Enrich IP entries with ISP, organization, and security flag data from ipregistry.co. When enabled, new IPs are automatically enriched on creation.

- -
- -
- -
- - -
Get your API key from ipregistry.co. Leave blank to use environment variable.
-
- -
- - -
-
-

@@ -1930,162 +1821,6 @@ if (function_exists('requireAuth')) {

- -
-

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 -
-
-
-
-
-
- - -
-

- - - - - - AWS Route53 Settings -

-

Configure AWS credentials and hosted zones for PTR record management.

- -
-
- - -
-
- - -
-
-
- - -
-
- - - Enter the hosted zone IDs for your forward DNS zones (A records) -
- -
- - -
- - -
-

Danger Zone

Irreversible actions - please proceed with caution.

@@ -2233,6 +1968,238 @@ if (function_exists('requireAuth')) {
+ +
+
+ +

Audit Log

+
+ +
+
+

View all changes made to geofeed entries including creates, updates, and deletes.

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

Integrations

+
+ + +
+

+ + + + + + AWS Route53 Settings +

+

Configure AWS credentials and hosted zones for PTR record management.

+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + Enter the hosted zone IDs for your forward DNS zones (A records) +
+ +
+ + +
+ + +
+ + +
+

+ + + + + + IP Registry Integration +

+

Enrich IP entries with ISP, organization, and security flag data from ipregistry.co. When enabled, new IPs are automatically enriched on creation.

+ +
+ +
+ +
+ + +
Get your API key from ipregistry.co. Leave blank to use environment variable.
+
+ +
+ + +
+
+ + +
+

+ + + + + n8n Webhook Integration +

+

Configure webhooks to notify n8n when geofeed data changes. Updates are debounced to batch multiple changes and reduce API calls.

+ +
+ +
+ +
+ + +
The n8n webhook URL to receive notifications
+
+ +
+ + +
Wait this many minutes after the last change before triggering the webhook (1-60 minutes)
+
+ +
+ + + +
+ + +
+
+

Webhook Queue

+
+ +
+
+
+
+
+
+
+
+
+
+
@@ -2246,6 +2213,99 @@ if (function_exists('requireAuth')) {

Developer Tools

+ +
+

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 +
+
+
+
+
+
+

@@ -2427,10 +2487,15 @@ if (function_exists('requireAuth')) {
- - + +
Displayed in the header and browser tab title
+
+ + +
Displayed as subtitle in the header
+
@@ -2444,7 +2509,7 @@ if (function_exists('requireAuth')) {
-
Pre-populated URL when importing from URL in the Advanced tab
+
Pre-populated URL when importing from URL in Developer tab
@@ -2470,8 +2535,8 @@ if (function_exists('requireAuth')) {
- Geofeed Manager -
PURPLE COMPUTING
+ ISP IP Manager +

@@ -2480,7 +2545,7 @@ if (function_exists('requireAuth')) { @@ -3756,6 +3821,85 @@ if (function_exists('requireAuth')) { window.location.href = 'api.php?action=export&format=download'; } + // Export Audit Log as CSV + async function exportAuditLog() { + showToast('Preparing audit log export...', 'info'); + + try { + // Fetch all audit log entries (high limit to get all) + const result = await api('audit_log', { page: 1, limit: 10000 }); + + if (!result.success || !result.data || result.data.length === 0) { + showToast('No audit log entries to export', 'error'); + return; + } + + // Build CSV content + const headers = ['Date/Time', 'Action', 'IP Prefix', 'Details', 'Changed By']; + const rows = result.data.map(entry => { + const date = new Date(entry.changed_at).toISOString(); + const action = entry.action; + + let ipPrefix = ''; + let details = ''; + + if (entry.new_values?.type === 'bulk_import') { + details = `Bulk import: ${entry.new_values.inserted} inserted, ${entry.new_values.updated} updated`; + } else if (entry.new_values?.type === 'url_import') { + details = `URL import from ${entry.new_values.url}: ${entry.new_values.inserted} inserted, ${entry.new_values.updated} updated`; + } else if (entry.new_values?.type === 'clear_all') { + details = `Cleared ${entry.old_values?.count || 0} entries`; + } else { + ipPrefix = entry.ip_prefix || entry.old_values?.ip_prefix || entry.new_values?.ip_prefix || ''; + + // Build details from old/new values + if (entry.action === 'UPDATE' && entry.old_values && entry.new_values) { + const changes = []; + for (const key of Object.keys(entry.new_values)) { + if (entry.old_values[key] !== entry.new_values[key]) { + changes.push(`${key}: ${entry.old_values[key] || '(empty)'} → ${entry.new_values[key] || '(empty)'}`); + } + } + details = changes.join('; '); + } else if (entry.action === 'INSERT' && entry.new_values) { + details = Object.entries(entry.new_values) + .filter(([k, v]) => v && k !== 'ip_prefix') + .map(([k, v]) => `${k}: ${v}`) + .join('; '); + } else if (entry.action === 'DELETE' && entry.old_values) { + details = Object.entries(entry.old_values) + .filter(([k, v]) => v && k !== 'ip_prefix') + .map(([k, v]) => `${k}: ${v}`) + .join('; '); + } + } + + const changedBy = entry.changed_by || 'Unknown'; + + return [date, action, ipPrefix, details, changedBy]; + }); + + // Create CSV string + const csvContent = [ + headers.join(','), + ...rows.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')) + ].join('\n'); + + // Download file + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `audit-log-${new Date().toISOString().split('T')[0]}.csv`; + link.click(); + URL.revokeObjectURL(link.href); + + showToast(`Exported ${result.data.length} audit log entries`, 'success'); + } catch (error) { + console.error('Audit log export error:', error); + showToast('Failed to export audit log', 'error'); + } + } + // File handling function handleFileSelect(input) { if (input.files && input.files[0]) { @@ -4600,11 +4744,13 @@ if (function_exists('requireAuth')) { try { const result = await api('whitelabel_get'); if (result.success && result.settings) { + document.getElementById('whitelabelAppName').value = result.settings.app_name || ''; document.getElementById('whitelabelCompanyName').value = result.settings.company_name || ''; document.getElementById('whitelabelIconUrl').value = result.settings.icon_url || ''; document.getElementById('whitelabelFaviconUrl').value = result.settings.favicon_url || ''; document.getElementById('whitelabelDefaultImportUrl').value = result.settings.default_import_url || ''; updateWhitelabelPreview(); + applyWhitelabelSettings(result.settings); } } catch (error) { console.error('Failed to load whitelabel settings:', error); @@ -4614,6 +4760,7 @@ if (function_exists('requireAuth')) { // Save whitelabel settings async function saveWhitelabelSettings() { const data = { + app_name: document.getElementById('whitelabelAppName').value, company_name: document.getElementById('whitelabelCompanyName').value, icon_url: document.getElementById('whitelabelIconUrl').value, favicon_url: document.getElementById('whitelabelFaviconUrl').value, @@ -4635,10 +4782,12 @@ if (function_exists('requireAuth')) { // Update the preview in the whitelabel tab function updateWhitelabelPreview() { - const companyName = document.getElementById('whitelabelCompanyName').value || 'Geofeed Manager'; + const appName = document.getElementById('whitelabelAppName').value || 'ISP IP Manager'; + const companyName = document.getElementById('whitelabelCompanyName').value || ''; const iconUrl = document.getElementById('whitelabelIconUrl').value; - document.getElementById('whitelabelPreviewName').textContent = companyName; + document.getElementById('whitelabelPreviewAppName').textContent = appName; + document.getElementById('whitelabelPreviewCompanyName').textContent = companyName; const previewIcon = document.getElementById('whitelabelPreviewIcon'); if (iconUrl) { @@ -4650,10 +4799,16 @@ if (function_exists('requireAuth')) { // Apply whitelabel settings to the page function applyWhitelabelSettings(settings) { - // Update page title - if (settings.company_name) { - document.title = settings.company_name; - document.querySelector('.logo-title').textContent = settings.company_name; + // Update page title and header + if (settings.app_name) { + document.title = settings.app_name; + document.querySelector('.logo-title').textContent = settings.app_name; + } + + // Update company name subtitle + const logoSubtitle = document.querySelector('.logo-subtitle'); + if (logoSubtitle) { + logoSubtitle.textContent = settings.company_name || ''; } // Update favicon @@ -4676,8 +4831,10 @@ if (function_exists('requireAuth')) { // Add event listeners for preview updates document.addEventListener('DOMContentLoaded', function() { + const appNameInput = document.getElementById('whitelabelAppName'); const companyNameInput = document.getElementById('whitelabelCompanyName'); const iconUrlInput = document.getElementById('whitelabelIconUrl'); + if (appNameInput) appNameInput.addEventListener('input', updateWhitelabelPreview); if (companyNameInput) companyNameInput.addEventListener('input', updateWhitelabelPreview); if (iconUrlInput) iconUrlInput.addEventListener('input', updateWhitelabelPreview); }); @@ -5100,17 +5257,13 @@ if (function_exists('requireAuth')) { return; } - // Group records by subnet (calculate /24 for IPv4) + // Group records by /24 subnet const subnets = {}; mismatchedOrMissing.forEach(record => { const ip = record.ip_address; - // Calculate /24 subnet (for IPv4) const parts = ip.split('.'); if (parts.length === 4) { - // Try /23 first by checking if this is an odd or even third octet - const thirdOctet = parseInt(parts[2]); - const subnetBase = thirdOctet % 2 === 0 ? thirdOctet : thirdOctet - 1; - const subnet = `${parts[0]}.${parts[1]}.${subnetBase}.0/23`; + const subnet = `${parts[0]}.${parts[1]}.${parts[2]}.0/24`; if (!subnets[subnet]) { subnets[subnet] = []; @@ -5123,27 +5276,40 @@ if (function_exists('requireAuth')) { } }); - const ipxoData = { - subnets: Object.keys(subnets).map(prefix => ({ - prefix: prefix, - records: subnets[prefix] - })) - }; + // Download a separate JSON file for each /24 subnet + const subnetKeys = Object.keys(subnets).sort(); + let downloadCount = 0; - const jsonStr = JSON.stringify(ipxoData, null, 2); + subnetKeys.forEach((subnet, index) => { + const ipxoData = { + subnets: [{ + prefix: subnet, + records: subnets[subnet] + }] + }; - // Copy to clipboard - navigator.clipboard.writeText(jsonStr).then(() => { - showToast(`Copied ${mismatchedOrMissing.length} PTR record(s) to clipboard in IPXO format`, 'success'); - }).catch(err => { - // Fallback for browsers that don't support clipboard API - const textArea = document.createElement('textarea'); - textArea.value = jsonStr; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand('copy'); - document.body.removeChild(textArea); - showToast(`Copied ${mismatchedOrMissing.length} PTR record(s) to clipboard in IPXO format`, 'success'); + const jsonStr = JSON.stringify(ipxoData, null, 2); + const blob = new Blob([jsonStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + // Create filename from subnet (replace / and . for valid filename) + const filename = `ipxo-ptr-${subnet.replace(/\//g, '-').replace(/\./g, '_')}.json`; + + // Stagger downloads slightly to avoid browser blocking + setTimeout(() => { + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + downloadCount++; + + if (downloadCount === subnetKeys.length) { + showToast(`Downloaded ${subnetKeys.length} IPXO JSON file(s) for ${mismatchedOrMissing.length} PTR record(s)`, 'success'); + } + }, index * 100); }); }