Files
ip-manager/webapp/settings.php
2026-01-18 02:17:26 +00:00

814 lines
42 KiB
PHP

<?php
/**
* Settings Page - Admin Only
*
* Contains: Integrations, Audit Log, Advanced, Whitelabel, Developer tabs
*/
// Include config
$configFile = __DIR__ . '/config.php';
if (file_exists($configFile)) {
require_once $configFile;
}
// Fallback CSRF
if (!function_exists('generateCSRFToken')) {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function generateCSRFToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
}
// Require authentication
if (function_exists('requireAuth')) {
requireAuth();
}
// Include auth helper
require_once __DIR__ . '/includes/auth.php';
// Require admin role
if (!isAdmin()) {
header('Location: index.php?error=unauthorized');
exit;
}
// Set page variables for header
$pageTitle = 'Settings - ISP IP Manager';
$currentPage = 'settings';
$showSettingsLink = false;
// Get current tab from URL
$currentTab = $_GET['tab'] ?? 'integrations';
$validTabs = ['integrations', 'users', 'audit', 'advanced', 'whitelabel', 'developer'];
if (!in_array($currentTab, $validTabs)) {
$currentTab = 'integrations';
}
// Include header
require_once __DIR__ . '/includes/header.php';
?>
<h1 class="settings-page-title">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
Settings
</h1>
<!-- Settings Navigation -->
<div class="settings-nav">
<a href="?tab=integrations" class="settings-nav-item <?php echo $currentTab === 'integrations' ? 'active' : ''; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
Integrations
</a>
<a href="?tab=users" class="settings-nav-item <?php echo $currentTab === 'users' ? 'active' : ''; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
Users
</a>
<a href="?tab=audit" class="settings-nav-item <?php echo $currentTab === 'audit' ? 'active' : ''; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
</svg>
Audit Log
</a>
<a href="?tab=advanced" class="settings-nav-item <?php echo $currentTab === 'advanced' ? 'active' : ''; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
Client Settings
</a>
<a href="?tab=whitelabel" class="settings-nav-item <?php echo $currentTab === 'whitelabel' ? 'active' : ''; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
Whitelabel
</a>
<a href="?tab=developer" class="settings-nav-item <?php echo $currentTab === 'developer' ? 'active' : ''; ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"/>
<polyline points="8 6 2 12 8 18"/>
</svg>
Developer
</a>
</div>
<!-- Integrations Section -->
<div class="settings-section <?php echo $currentTab === 'integrations' ? 'active' : ''; ?>" id="section-integrations">
<!-- AWS Route53 Settings -->
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>
AWS Route53 Settings
</h2>
<p class="advanced-section-desc">Configure AWS credentials and hosted zones for PTR record management.</p>
<div class="form-grid" style="margin-top: 16px;">
<div class="form-group">
<label class="form-label">AWS Access Key ID</label>
<input type="text" class="form-input" id="awsAccessKeyId" placeholder="AKIAIOSFODNN7EXAMPLE">
</div>
<div class="form-group">
<label class="form-label">AWS Secret Access Key</label>
<input type="password" class="form-input" id="awsSecretAccessKey" placeholder="••••••••••••••••">
</div>
</div>
<div class="form-group">
<label class="form-label">AWS Region</label>
<select class="form-select" id="awsRegion">
<option value="us-east-1">US East (N. Virginia)</option>
<option value="us-east-2">US East (Ohio)</option>
<option value="us-west-1">US West (N. California)</option>
<option value="us-west-2">US West (Oregon)</option>
<option value="eu-west-1">EU (Ireland)</option>
<option value="eu-west-2">EU (London)</option>
<option value="eu-central-1">EU (Frankfurt)</option>
<option value="ap-southeast-1">Asia Pacific (Singapore)</option>
<option value="ap-southeast-2">Asia Pacific (Sydney)</option>
<option value="ap-northeast-1">Asia Pacific (Tokyo)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Route53 Hosted Zone IDs (comma separated)</label>
<input type="text" class="form-input" id="awsHostedZones" placeholder="Z1234567890ABC, Z0987654321DEF">
<small style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">Enter the hosted zone IDs for your forward DNS zones (A records)</small>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px;">
<button class="btn btn-primary" onclick="saveAwsSettings()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Save AWS Settings
</button>
<button class="btn btn-secondary" onclick="testAwsConnection()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
Test Connection
</button>
</div>
<div id="awsTestResult" style="margin-top: 16px; display: none;"></div>
</div>
<!-- IP Registry Settings -->
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>
IP Registry Integration
</h2>
<p class="advanced-section-desc">Enrich IP entries with ISP, organization, and security flag data from <a href="https://ipregistry.co" target="_blank" rel="noopener" style="color: var(--purple-primary);">ipregistry.co</a>.</p>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="ipRegistryEnabled" style="margin-right: 8px; vertical-align: middle;">
Enable IP Registry Auto-Enrichment
</label>
</div>
<div class="form-group">
<label class="form-label">API Key</label>
<input type="password" class="form-input" id="ipRegistryApiKey" placeholder="Enter your ipregistry.co API key">
<div class="form-hint">Get your API key from <a href="https://ipregistry.co" target="_blank" rel="noopener" style="color: var(--purple-primary);">ipregistry.co</a>.</div>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="saveIpRegistrySettings()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Save Settings
</button>
<button class="btn btn-secondary" onclick="enrichAllIps(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>
Enrich All Un-enriched IPs
</button>
</div>
</div>
<!-- n8n Webhook Settings -->
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
n8n Webhook Integration
</h2>
<p class="advanced-section-desc">Configure webhooks to notify n8n when geofeed data changes.</p>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="webhookEnabled" style="margin-right: 8px; vertical-align: middle;">
Enable Webhook Notifications
</label>
</div>
<div class="form-group">
<label class="form-label">Webhook URL</label>
<input type="url" class="form-input" id="webhookUrl" placeholder="https://your-n8n-instance.com/webhook/xxx">
</div>
<div class="form-group">
<label class="form-label">Debounce Delay (minutes)</label>
<input type="number" class="form-input" id="webhookDelay" min="1" max="60" value="3" style="max-width: 120px;">
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;">
<button class="btn btn-primary" onclick="saveWebhookSettings()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Save Settings
</button>
<button class="btn btn-secondary" onclick="testWebhook()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
Test Connection
</button>
<button class="btn btn-secondary" onclick="triggerWebhookNow()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
Trigger Now
</button>
</div>
<div class="table-container" style="margin-top: 16px;">
<div class="table-header">
<h3 class="table-title">Webhook Queue</h3>
<button class="btn btn-ghost btn-sm" onclick="loadWebhookQueueStatus()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Refresh
</button>
</div>
<div id="webhookQueueContainer" style="padding: 20px;">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
</div>
</div>
<!-- Users Section -->
<div class="settings-section <?php echo $currentTab === 'users' ? 'active' : ''; ?>" id="section-users">
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
User Management
</h2>
<p class="advanced-section-desc">Manage admin and staff users who can access this application. Users are authenticated via Cloudflare Access.</p>
<!-- Add User Form -->
<div class="form-grid" style="margin-top: 16px; margin-bottom: 24px;">
<div class="form-group">
<label class="form-label">Email Address</label>
<input type="email" class="form-input" id="newUserEmail" placeholder="user@example.com">
</div>
<div class="form-group">
<label class="form-label">Display Name (optional)</label>
<input type="text" class="form-input" id="newUserDisplayName" placeholder="John Doe">
</div>
</div>
<div class="form-group" style="margin-bottom: 16px;">
<label class="form-label">Role</label>
<select class="form-select" id="newUserRole" style="max-width: 300px;">
<option value="staff">Staff - Can view and edit Geofeed/PTR entries</option>
<option value="admin">Admin - Full access including settings</option>
</select>
</div>
<button class="btn btn-primary" onclick="addUser()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Add User
</button>
<!-- Users Table -->
<div class="table-container" style="margin-top: 24px;">
<table class="data-table" id="usersTable">
<thead>
<tr>
<th>Email</th>
<th>Display Name</th>
<th>Role</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="usersTableBody">
<tr>
<td colspan="6" style="text-align: center; padding: 40px;">
<div class="loading"><div class="spinner"></div></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Audit Log Section -->
<div class="settings-section <?php echo $currentTab === 'audit' ? 'active' : ''; ?>" id="section-audit">
<div class="advanced-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<p class="advanced-section-desc" style="margin: 0;">View all changes made to geofeed entries including creates, updates, and deletes.</p>
<button class="btn btn-secondary btn-sm" onclick="exportAuditLog()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Export CSV
</button>
</div>
<div class="table-container">
<div class="audit-log-container" id="auditLogContainer">
<div class="loading"><div class="spinner"></div></div>
</div>
<div class="pagination" id="auditPagination" style="display: none;"></div>
</div>
</div>
</div>
<!-- Advanced Section -->
<div class="settings-section <?php echo $currentTab === 'advanced' ? 'active' : ''; ?>" id="section-advanced">
<!-- Client Shortnames -->
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
Client Shortnames
</h2>
<p class="advanced-section-desc">Client shortnames currently in use across geofeed entries.</p>
<div id="shortnamesContainer" style="margin-top: 16px;">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
<!-- Company Logos -->
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
Company Logos
</h2>
<p class="advanced-section-desc">Manage logo images for client shortnames. Logos will appear in the entries table as 1:1 square icons.</p>
<div class="form-row" style="margin-bottom: 16px; margin-top: 16px;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">Client Short Name</label>
<select class="form-select" id="logoShortName">
<option value="">Select or type a shortname...</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">Logo URL (PNG)</label>
<input type="url" class="form-input" id="logoUrl" placeholder="https://example.com/logo.png">
</div>
</div>
<button class="btn btn-primary" onclick="saveLogo()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Save Logo
</button>
<div class="logo-grid" id="logoGrid" style="margin-top: 16px;">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
</div>
<!-- Whitelabel Section -->
<div class="settings-section <?php echo $currentTab === 'whitelabel' ? 'active' : ''; ?>" id="section-whitelabel">
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
Branding
</h2>
<p class="advanced-section-desc">Customize the appearance of the application with your company branding.</p>
<div class="form-grid" style="margin-top: 16px;">
<div class="form-group">
<label class="form-label">App Name</label>
<input type="text" class="form-input" id="whitelabelAppName" placeholder="ISP IP Manager">
<div class="form-hint">Displayed in the header and browser tab title</div>
</div>
<div class="form-group">
<label class="form-label">Company Name</label>
<input type="text" class="form-input" id="whitelabelCompanyName" placeholder="Your Company">
<div class="form-hint">Displayed as subtitle in the header</div>
</div>
<div class="form-group">
<label class="form-label">Webapp Icon URL</label>
<input type="text" class="form-input" id="whitelabelIconUrl" placeholder="https://example.com/logo.svg">
<div class="form-hint">URL to an SVG or PNG logo for the header</div>
</div>
<div class="form-group">
<label class="form-label">Favicon URL</label>
<input type="text" class="form-input" id="whitelabelFaviconUrl" placeholder="https://example.com/favicon.ico">
<div class="form-hint">URL to a favicon (.ico, .png, or .svg)</div>
</div>
<div class="form-group">
<label class="form-label">Default Geofeed Import URL</label>
<input type="text" class="form-input" id="whitelabelDefaultImportUrl" placeholder="https://example.com/geofeed.csv">
<div class="form-hint">Pre-populated URL when importing from URL</div>
</div>
</div>
<div style="display: flex; gap: 12px; margin-top: 16px;">
<button class="btn btn-primary" onclick="saveWhitelabelSettings()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Save Settings
</button>
</div>
<!-- Preview -->
<div style="margin-top: 24px; padding: 16px; background: var(--bg-tertiary); border-radius: var(--radius-md);">
<h3 style="font-size: 14px; font-weight: 600; color: var(--text-secondary); margin-bottom: 12px;">Preview</h3>
<div style="display: flex; align-items: center; gap: 12px; padding: 12px; background: var(--bg-secondary); border-radius: var(--radius-md);">
<div id="whitelabelPreviewIcon" style="width: 40px; height: 40px; background: var(--purple-primary); border-radius: 8px; display: flex; align-items: center; justify-content: center; overflow: hidden;">
<svg viewBox="0 0 258 258" fill="none" width="24" height="24">
<path fill="white" d="M241.13 56.2A26.53 26.53 0 11188.07 56.2a26.53 26.53 0 0153.06 0zm-5.34-.05a21.19 21.19 0 10-42.38 0 21.19 21.19 0 0042.38 0z" transform="translate(-30,-5) scale(0.75)"/>
<path fill="white" d="M21.42 37.38h55.28a.32.32 0 01.32.32v12.21a.46.46 0 00.8.3c13.2-14.73 32.09-17.47 50.68-12.7 35.19 9.03 47.69 43.89 45.07 77C170.91 148.16 150.93 173.81 115.1 175.14q-22.52.84-37.38-15.22a.65.65 0 00-1.13.47c.06 1.2.49 2.44.49 4.15q-.04 23.9.01 56.37a.42.41 0 01-.42.41H21.66a.88.88 0 01-.88-.88V38.01a.64.63 0 01.64-.63zM77.02 104.64c0 12.43 5.67 26.28 20.24 26.28s20.25-13.85 20.25-26.28-5.67-26.28-20.25-26.28-20.24 13.85-20.24 26.28z" transform="translate(30,30) scale(0.75)"/>
</svg>
</div>
<div>
<span id="whitelabelPreviewAppName" style="font-weight: 600; color: var(--text-primary);">ISP IP Manager</span>
<div id="whitelabelPreviewCompanyName" style="font-size: 11px; color: var(--text-tertiary);"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Developer Section -->
<div class="settings-section <?php echo $currentTab === 'developer' ? 'active' : ''; ?>" id="section-developer">
<!-- Import Section -->
<div class="advanced-section">
<h2 class="advanced-section-title">Import Geofeed Data</h2>
<p class="advanced-section-desc">Import geofeed entries from a CSV file or a remote URL.</p>
<div class="import-options" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-top: 16px;">
<!-- File Upload -->
<div class="import-card" style="padding: 20px; background: var(--bg-tertiary); border-radius: var(--radius-md); border: 2px dashed var(--border-strong);">
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 12px;">Upload CSV File</h3>
<input type="file" id="csvFileInput" accept=".csv,.txt" style="display: none;" onchange="handleFileUpload(this)">
<button class="btn btn-secondary" onclick="document.getElementById('csvFileInput').click()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
Choose File
</button>
<div id="fileUploadStatus" style="margin-top: 8px; font-size: 12px; color: var(--text-secondary);"></div>
</div>
<!-- URL Import -->
<div class="import-card" style="padding: 20px; background: var(--bg-tertiary); border-radius: var(--radius-md); border: 2px dashed var(--border-strong);">
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 12px;">Import from URL</h3>
<div style="display: flex; gap: 8px;">
<input type="url" class="form-input" id="importUrl" placeholder="https://example.com/geofeed.csv" style="flex: 1;">
<button class="btn btn-primary" onclick="importFromUrl()">Import</button>
</div>
<div id="urlImportStatus" style="margin-top: 8px; font-size: 12px; color: var(--text-secondary);"></div>
</div>
</div>
</div>
<!-- System Info -->
<div class="advanced-section">
<h2 class="advanced-section-title">System Information</h2>
<div id="systemInfoContent" style="margin-top: 16px;">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
<!-- Error Logs -->
<div class="advanced-section">
<h2 class="advanced-section-title">Error Logs</h2>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div style="display: flex; gap: 8px; align-items: center;">
<label class="form-label" style="margin: 0;">Show last</label>
<select id="errorLogLines" class="form-input" style="width: auto;" onchange="loadErrorLogs()">
<option value="50">50 lines</option>
<option value="100" selected>100 lines</option>
<option value="250">250 lines</option>
<option value="500">500 lines</option>
</select>
</div>
<button class="btn btn-secondary btn-sm" onclick="clearErrorLogs()">Clear Logs</button>
</div>
<div id="errorLogsContent" style="margin-top: 16px;">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
<!-- Danger Zone -->
<div class="advanced-section" style="border: 2px solid var(--error); background: var(--error-bg);">
<h2 class="advanced-section-title" style="color: var(--error);">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
Danger Zone
</h2>
<p class="advanced-section-desc">Irreversible actions. Proceed with caution.</p>
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px;">
<button class="btn btn-danger" onclick="confirmClearAll()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
Clear All Entries
</button>
</div>
</div>
</div>
<?php
// Set additional scripts to be loaded AFTER app.js
$additionalScripts = '<script>
// 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 "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 = \'<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--text-tertiary);">No users found. Add a user above.</td></tr>\';
return;
}
tbody.innerHTML = data.users.map(user => `
<tr data-user-id="${user.id}">
<td>${escapeHtml(user.email)}</td>
<td>${user.display_name ? escapeHtml(user.display_name) : \'<span style="color: var(--text-tertiary);">-</span>\'}</td>
<td>
<span class="badge ${user.role === \'admin\' ? \'badge-purple\' : \'badge-blue\'}">
${user.role === \'admin\' ? \'Admin\' : \'Staff\'}
</span>
</td>
<td>
<span class="badge ${user.active == 1 ? \'badge-success\' : \'badge-warning\'}">
${user.active == 1 ? \'Active\' : \'Inactive\'}
</span>
</td>
<td>${formatDate(user.created_at)}</td>
<td>
<div style="display: flex; gap: 8px;">
<button class="btn btn-secondary btn-sm" onclick="toggleUser(${user.id})" title="${user.active == 1 ? \'Deactivate\' : \'Activate\'}">
${user.active == 1 ?
\'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>\' :
\'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>\'
}
</button>
<button class="btn btn-danger btn-sm" onclick="deleteUser(${user.id}, \'${escapeHtml(user.email)}\')" title="Delete">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
</div>
</td>
</tr>
`).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"
});
}
</script>';
// Include footer
require_once __DIR__ . '/includes/footer.php';
?>