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
+
-
-
-
-
- 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.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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.
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -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
-
-
-
-
-
-
-
-
-
-
-
- Import Complete
-
-
-
-
-
-
-
-
-
Import from URL
-
Fetch and import a geofeed from a remote URL
-
-
-
-
-
-
-
-
-
-
-
-
- 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ n8n Webhook Integration
+
+
Configure webhooks to notify n8n when geofeed data changes. Updates are debounced to batch multiple changes and reduce API calls.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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
+
+
+
+
+
+
+
+
+
+
+
+ Import Complete
+
+
+
+
+
+
+
+
+
Import from URL
+
Fetch and import a geofeed from a remote URL
+
+
+
+
+
+
+
+
+
+
+
+
+ Import Complete
+
+
+
+
+
+
+
@@ -2427,10 +2487,15 @@ 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);
});
}