Settings

Integrations Users Audit Log Client Settings Whitelabel License Developer

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.

Get your API key from ipregistry.co.

n8n Webhook Integration

Configure webhooks to notify n8n when geofeed data changes.

Webhook Queue

User Management

Manage admin and staff users who can access this application. Users are authenticated via Cloudflare Access.

Email Display Name Role Status Created Actions

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

Client Shortnames

Client shortnames currently in use across geofeed entries.

Company Logos

Manage logo images for client shortnames. Logos will appear in the entries table as 1:1 square icons.

Branding

Customize the appearance of the application with your company branding.

Displayed in the header and browser tab title
Displayed as subtitle in the header
URL to an SVG or PNG logo for the header
URL to a favicon (.ico, .png, or .svg)
Pre-populated URL when importing from URL

Preview

ISP IP Manager

License Status

View and manage your ISP IP Manager license.

Usage Statistics

Current usage against your license limits.

Activate License

Enter your license key to activate or upgrade your plan.

Available Plans

Compare features across different license tiers.

Trial

14 days free

  • ✓ 100 entries
  • ✓ 2 users
  • ✓ Basic CRUD
  • ✓ CSV export

Basic

Small ISPs

  • ✓ 500 entries
  • ✓ 5 users
  • ✓ Webhooks
  • ✓ Audit log

Professional

Growing ISPs

  • ✓ 2,500 entries
  • ✓ 15 users
  • ✓ IP enrichment
  • ✓ Whitelabel
  • ✓ PTR records

Enterprise

Large ISPs

  • ✓ Unlimited entries
  • ✓ Unlimited users
  • ✓ API access
  • ✓ Priority support

Contact info@purplecomputing.com for pricing and license keys.

Import Geofeed Data

Import geofeed entries from a CSV file or a remote URL.

Upload CSV File

Import from URL

Database Schema Updates

Check for and apply missing database columns or tables from the repository schema.

Database Backup & Restore

Export or import a full database backup as JSON (entries, settings, logos, audit log).

WebDAV Backup

Backup database to a WebDAV server (e.g., Nextcloud public share).

For Nextcloud: https://your-server/public.php/webdav (the filename will be appended automatically)

System Information

Error Logs

Danger Zone

Irreversible actions. Proceed with caution.

// Notification helper - uses showToast from app.js if available, otherwise creates simple alert function showNotification(message, type = "success") { if (typeof showToast === "function") { showToast(message, type); } else { // Fallback toast implementation const container = document.getElementById("toastContainer") || document.body; const toast = document.createElement("div"); toast.className = `toast ${type}`; toast.style.cssText = "position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; border-radius: 8px; color: white; z-index: 10000; animation: slideIn 0.3s ease;"; toast.style.background = type === "error" ? "#dc3545" : type === "info" ? "#17a2b8" : "#28a745"; toast.textContent = message; container.appendChild(toast); setTimeout(() => toast.remove(), 4000); } } // Initialize settings page document.addEventListener("DOMContentLoaded", function() { const currentTab = "' . $currentTab . '"; // Load data for the current tab switch(currentTab) { case "integrations": loadAwsSettings(); loadIpRegistrySettings(); loadWebhookSettings(); loadWebhookQueueStatus(); break; case "users": loadUsers(); break; case "audit": loadAuditLog(); break; case "advanced": loadShortnamesGrid(); loadShortnames(); loadLogosGrid(); break; case "whitelabel": loadWhitelabelSettings(); break; case "license": loadLicenseStatus(); loadLicenseUsage(); break; case "developer": loadSystemInfo(); loadErrorLogs(); break; } }); // User Management Functions async function loadUsers() { try { const response = await fetch("api.php?action=admin_users_list", { headers: { "X-CSRF-Token": CSRF_TOKEN } }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to load users"); } const tbody = document.getElementById("usersTableBody"); if (data.users.length === 0) { tbody.innerHTML = \'No users found. Add a user above.\'; return; } tbody.innerHTML = data.users.map(user => ` ${escapeHtml(user.email)} ${user.display_name ? escapeHtml(user.display_name) : \'-\'} ${user.role === \'admin\' ? \'Admin\' : \'Staff\'} ${user.active == 1 ? \'Active\' : \'Inactive\'} ${formatDate(user.created_at)}
`).join(\'\'); } catch (error) { console.error("Error loading users:", error); showNotification("Failed to load users: " + error.message, "error"); } } async function addUser() { const email = document.getElementById("newUserEmail").value.trim(); const displayName = document.getElementById("newUserDisplayName").value.trim(); const role = document.getElementById("newUserRole").value; if (!email) { showNotification("Please enter an email address", "error"); return; } try { const response = await fetch("api.php?action=admin_user_save", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": CSRF_TOKEN }, body: JSON.stringify({ email: email, display_name: displayName || null, role: role }) }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to add user"); } showNotification("User added successfully", "success"); document.getElementById("newUserEmail").value = ""; document.getElementById("newUserDisplayName").value = ""; document.getElementById("newUserRole").value = "staff"; loadUsers(); } catch (error) { console.error("Error adding user:", error); showNotification("Failed to add user: " + error.message, "error"); } } async function toggleUser(userId) { try { const response = await fetch("api.php?action=admin_user_toggle", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": CSRF_TOKEN }, body: JSON.stringify({ id: userId }) }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to toggle user status"); } showNotification("User status updated", "success"); loadUsers(); } catch (error) { console.error("Error toggling user:", error); showNotification("Failed to toggle user: " + error.message, "error"); } } async function deleteUser(userId, email) { if (!confirm("Are you sure you want to delete the user \"" + email + "\"? This action cannot be undone.")) { return; } try { const response = await fetch("api.php?action=admin_user_delete", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": CSRF_TOKEN }, body: JSON.stringify({ id: userId }) }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to delete user"); } showNotification("User deleted successfully", "success"); loadUsers(); } catch (error) { console.error("Error deleting user:", error); showNotification("Failed to delete user: " + error.message, "error"); } } function escapeHtml(text) { if (!text) return ""; const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } function formatDate(dateStr) { if (!dateStr) return "-"; const date = new Date(dateStr); return date.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }); } // License Management Functions async function loadLicenseStatus() { try { const response = await fetch("api.php?action=license_status", { headers: { "X-CSRF-Token": CSRF_TOKEN } }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to load license status"); } const container = document.getElementById("licenseStatusContainer"); const license = data.license; let statusBadge = ""; let statusClass = ""; switch (license.status) { case "active": statusBadge = "Active"; statusClass = "badge-success"; break; case "expired": statusBadge = "Expired"; statusClass = "badge-error"; break; case "inactive": statusBadge = "Inactive"; statusClass = "badge-warning"; break; case "unlicensed": statusBadge = "Unlicensed"; statusClass = "badge-gray"; break; default: statusBadge = license.status; statusClass = "badge-gray"; } let expiryInfo = ""; if (license.expires_at) { const expiryDate = new Date(license.expires_at); expiryInfo = `

Expires: ${expiryDate.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })} ${license.days_remaining !== null ? `(${license.days_remaining} days remaining)` : ""}

`; } let licenseDetails = ""; if (license.license && license.license.licensee_name) { licenseDetails = `

Licensee: ${escapeHtml(license.license.licensee_name)}

Email: ${escapeHtml(license.license.licensee_email)}

License Key: ${maskLicenseKey(license.license.license_key)}

`; // Show deactivate button document.getElementById("deactivateLicenseBtn").style.display = "inline-flex"; } else { document.getElementById("deactivateLicenseBtn").style.display = "none"; } container.innerHTML = `
${statusBadge} ${license.limits?.name || "Unknown"} License

${license.message}

${expiryInfo} ${licenseDetails} `; } catch (error) { console.error("Error loading license status:", error); document.getElementById("licenseStatusContainer").innerHTML = `
Failed to load license status: ${escapeHtml(error.message)}
`; } } function maskLicenseKey(key) { if (!key || key.length < 10) return key; return key.substring(0, 10) + "..." + key.substring(key.length - 4); } async function loadLicenseUsage() { try { const response = await fetch("api.php?action=license_usage", { headers: { "X-CSRF-Token": CSRF_TOKEN } }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to load license usage"); } const container = document.getElementById("licenseUsageContainer"); const usage = data.usage; const entriesBar = renderUsageBar(usage.entries); const usersBar = renderUsageBar(usage.users); container.innerHTML = `
Geofeed Entries ${usage.entries.current} / ${usage.entries.unlimited ? "Unlimited" : usage.entries.max}
${entriesBar}
Users ${usage.users.current} / ${usage.users.unlimited ? "Unlimited" : usage.users.max}
${usersBar}
`; } catch (error) { console.error("Error loading license usage:", error); document.getElementById("licenseUsageContainer").innerHTML = `
Failed to load usage data: ${escapeHtml(error.message)}
`; } } function renderUsageBar(usage) { if (usage.unlimited) { return `
`; } const percentage = Math.min(100, usage.percentage); let barColor = "var(--purple-primary)"; if (percentage >= 90) { barColor = "var(--error)"; } else if (percentage >= 75) { barColor = "var(--warning)"; } return `
`; } async function activateLicense() { const licenseKey = document.getElementById("licenseKey").value.trim(); const licenseeName = document.getElementById("licenseeName").value.trim(); const licenseeEmail = document.getElementById("licenseeEmail").value.trim(); if (!licenseKey) { showNotification("Please enter a license key", "error"); return; } if (!licenseeName) { showNotification("Please enter the licensee name", "error"); return; } if (!licenseeEmail) { showNotification("Please enter the licensee email", "error"); return; } try { const response = await fetch("api.php?action=license_activate", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": CSRF_TOKEN }, body: JSON.stringify({ license_key: licenseKey, licensee_name: licenseeName, licensee_email: licenseeEmail }) }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to activate license"); } showNotification(data.message || "License activated successfully", "success"); // Clear the form document.getElementById("licenseKey").value = ""; document.getElementById("licenseeName").value = ""; document.getElementById("licenseeEmail").value = ""; // Reload license status loadLicenseStatus(); loadLicenseUsage(); } catch (error) { console.error("Error activating license:", error); showNotification("Failed to activate license: " + error.message, "error"); } } async function deactivateLicense() { if (!confirm("Are you sure you want to deactivate your license? This will revert your account to trial mode.")) { return; } try { const response = await fetch("api.php?action=license_deactivate", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": CSRF_TOKEN } }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to deactivate license"); } showNotification(data.message || "License deactivated", "success"); // Reload license status loadLicenseStatus(); loadLicenseUsage(); } catch (error) { console.error("Error deactivating license:", error); showNotification("Failed to deactivate license: " + error.message, "error"); } } // Schema Update Functions let pendingSchemaChanges = []; async function checkSchemaUpdates() { const resultContainer = document.getElementById("schemaCheckResult"); const applyBtn = document.getElementById("applySchemaBtn"); resultContainer.style.display = "block"; resultContainer.innerHTML = \'
\'; applyBtn.style.display = "none"; try { const response = await fetch("api.php?action=schema_check", { headers: { "X-CSRF-Token": CSRF_TOKEN } }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to check schema"); } if (data.changes && data.changes.length > 0) { pendingSchemaChanges = data.changes; let changesHtml = \'
\'; changesHtml += \'Schema updates available:\'; changesHtml += \'
\'; resultContainer.innerHTML = changesHtml; applyBtn.style.display = "inline-flex"; } else { resultContainer.innerHTML = \'
Database schema is up to date!
\'; } } catch (error) { console.error("Error checking schema:", error); resultContainer.innerHTML = `
Error: ${escapeHtml(error.message)}
`; } } async function applySchemaUpdates() { const resultContainer = document.getElementById("schemaCheckResult"); const applyBtn = document.getElementById("applySchemaBtn"); if (!confirm("Are you sure you want to apply the schema changes? This will modify your database structure.")) { return; } resultContainer.innerHTML = \'
\'; applyBtn.disabled = true; try { const response = await fetch("api.php?action=schema_apply", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": CSRF_TOKEN }, credentials: "same-origin", body: JSON.stringify({ csrf_token: CSRF_TOKEN }) }); const text = await response.text(); let data; try { data = JSON.parse(text); } catch (parseError) { console.error("Failed to parse response:", text); throw new Error("Server returned invalid response. Check error logs."); } if (!data.success) { throw new Error(data.error || "Failed to apply schema changes"); } let resultsHtml = \'
\'; resultsHtml += \'Schema changes applied successfully!\'; if (data.results && data.results.length > 0) { resultsHtml += \'\'; } resultsHtml += \'
\'; resultContainer.innerHTML = resultsHtml; applyBtn.style.display = "none"; pendingSchemaChanges = []; showNotification("Schema changes applied successfully", "success"); } catch (error) { console.error("Error applying schema:", error); resultContainer.innerHTML = `
Error: ${escapeHtml(error.message)}
`; applyBtn.disabled = false; } } // Database Backup/Restore Functions async function downloadBackup() { try { showNotification("Generating backup...", "info"); const response = await fetch("api.php?action=backup_export", { headers: { "X-CSRF-Token": CSRF_TOKEN }, credentials: "same-origin" }); if (!response.ok) { throw new Error("Failed to generate backup"); } const data = await response.json(); if (!data.success) { throw new Error(data.error || "Backup failed"); } // Create download const blob = new Blob([JSON.stringify(data.backup, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); a.href = url; a.download = `ipmanager-backup-${timestamp}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification("Backup downloaded successfully", "success"); } catch (error) { console.error("Backup error:", error); showNotification("Backup failed: " + error.message, "error"); } } async function handleRestoreFile(input) { const file = input.files[0]; if (!file) return; if (!confirm("WARNING: This will replace all existing data with the backup. Are you sure you want to continue?")) { input.value = ""; return; } const statusDiv = document.getElementById("backupRestoreStatus"); statusDiv.style.display = "block"; statusDiv.innerHTML = \'
\'; try { const text = await file.text(); const backupData = JSON.parse(text); const response = await fetch("api.php?action=backup_import", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": CSRF_TOKEN }, credentials: "same-origin", body: JSON.stringify({ backup_data: backupData, csrf_token: CSRF_TOKEN }) }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Restore failed"); } statusDiv.innerHTML = \'
Restore completed successfully!
\'; showNotification("Database restored successfully", "success"); } catch (error) { console.error("Restore error:", error); statusDiv.innerHTML = `
Restore failed: ${escapeHtml(error.message)}
`; } input.value = ""; } // WebDAV Backup Functions async function loadWebDAVSettings() { try { const response = await fetch("api.php?action=webdav_settings_get", { headers: { "X-CSRF-Token": CSRF_TOKEN }, credentials: "same-origin" }); const data = await response.json(); if (data.success) { document.getElementById("webdavServerUrl").value = data.settings.webdav_server_url || ""; document.getElementById("webdavUsername").value = data.settings.webdav_username || ""; // Password is not returned for security } } catch (error) { console.error("Failed to load WebDAV settings:", error); } } async function saveWebDAVSettings() { const serverUrl = document.getElementById("webdavServerUrl").value.trim(); const username = document.getElementById("webdavUsername").value.trim(); const password = document.getElementById("webdavPassword").value; try { const response = await fetch("api.php?action=webdav_settings_save", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": CSRF_TOKEN }, credentials: "same-origin", body: JSON.stringify({ webdav_server_url: serverUrl, webdav_username: username, webdav_password: password, csrf_token: CSRF_TOKEN }) }); const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to save settings"); } showNotification("WebDAV settings saved", "success"); } catch (error) { console.error("Save WebDAV settings error:", error); showNotification("Failed to save settings: " + error.message, "error"); } } async function backupToWebDAV() { const serverUrl = document.getElementById("webdavServerUrl").value.trim(); const username = document.getElementById("webdavUsername").value.trim(); const password = document.getElementById("webdavPassword").value; if (!serverUrl || !username) { showNotification("Please enter WebDAV server URL and username", "error"); return; } const statusDiv = document.getElementById("webdavBackupStatus"); statusDiv.style.display = "block"; statusDiv.innerHTML = \'
\'; try { const response = await fetch("api.php?action=webdav_backup", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": CSRF_TOKEN }, credentials: "same-origin", body: JSON.stringify({ webdav_server_url: serverUrl, webdav_username: username, webdav_password: password, csrf_token: CSRF_TOKEN }) }); const text = await response.text(); let data; try { data = JSON.parse(text); } catch (parseError) { console.error("Failed to parse response:", text); throw new Error("Server returned invalid response. Check error logs."); } if (!data.success) { // Include extra details if available let errorMsg = data.error || "WebDAV backup failed"; if (data.url) { errorMsg += ` (URL: ${data.url})`; } if (data.http_code) { errorMsg += ` (HTTP: ${data.http_code})`; } throw new Error(errorMsg); } statusDiv.innerHTML = `
Backup uploaded successfully!
Filename: ${escapeHtml(data.filename)}
`; showNotification("Backup uploaded to WebDAV", "success"); } catch (error) { console.error("WebDAV backup error:", error); statusDiv.innerHTML = `
WebDAV backup failed: ${escapeHtml(error.message)}
`; } } // Load WebDAV settings when on developer tab if (document.getElementById("webdavServerUrl")) { loadWebDAVSettings(); } '; // Include footer require_once __DIR__ . '/includes/footer.php'; ?>