Settings
AWS Route53 Settings
Configure AWS credentials and hosted zones for PTR record management.
AWS Region
US East (N. Virginia)
US East (Ohio)
US West (N. California)
US West (Oregon)
EU (Ireland)
EU (London)
EU (Frankfurt)
Asia Pacific (Singapore)
Asia Pacific (Sydney)
Asia Pacific (Tokyo)
Route53 Hosted Zone IDs (comma separated)
Enter the hosted zone IDs for your forward DNS zones (A records)
Save AWS Settings
Test Connection
User Management
Manage admin and staff users who can access this application. Users are authenticated via Cloudflare Access.
Role
Staff - Can view and edit Geofeed/PTR entries
Admin - Full access including settings
Add User
Email
Display Name
Role
Status
Created
Actions
View all changes made to geofeed entries including creates, updates, and deletes.
Export CSV
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.
Save Logo
Branding
Customize the appearance of the application with your company branding.
License Status
View and manage your ISP IP Manager license.
Usage Statistics
Current usage against your license limits.
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.
Database Schema Updates
Check for and apply missing database columns or tables from the repository schema.
Check for Updates
Apply Schema Changes
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).
WebDAV URL (full path)
For Nextcloud: https://your-server/public.php/webdav (the filename will be appended automatically)
Backup to WebDAV
Save Settings
Error Logs
Show last
50 lines
100 lines
250 lines
500 lines
Clear 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)}
${user.active == 1 ?
\' \' :
\' \'
}
`).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 += \'
\';
data.changes.forEach(change => {
changesHtml += `${escapeHtml(change.description || change.type + \': \' + change.name)} `;
});
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 += \'
\';
data.results.forEach(result => {
const icon = result.success ? \'✓\' : \'✗\';
const color = result.success ? \'var(--success)\' : \'var(--error)\';
resultsHtml += `${icon} ${escapeHtml(result.description || result.name)} `;
});
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';
?>