diff --git a/README.md b/README.md index 4771d77..7488452 100644 --- a/README.md +++ b/README.md @@ -405,6 +405,81 @@ Ensure your IP prefixes are in valid CIDR notation (e.g., `192.168.1.0/24`) - Click "Check for Updates" - Apply any missing schema changes +## Licensing + +ISP IP Manager uses a tiered licensing system. Each deployment requires a valid license key. + +### License Tiers + +| Feature | Trial | Basic | Professional | Enterprise | +|---------|-------|-------|--------------|------------| +| Duration | 14 days | 1 year | 1 year | Perpetual | +| Max Entries | 100 | 500 | 2,500 | Unlimited | +| Max Users | 2 | 5 | 15 | Unlimited | +| Basic CRUD | ✓ | ✓ | ✓ | ✓ | +| CSV Export | ✓ | ✓ | ✓ | ✓ | +| Webhooks | ✗ | ✓ | ✓ | ✓ | +| Audit Log | ✗ | ✓ | ✓ | ✓ | +| IP Enrichment | ✗ | ✗ | ✓ | ✓ | +| Whitelabel | ✗ | ✗ | ✓ | ✓ | +| PTR Records | ✗ | ✗ | ✓ | ✓ | +| API Access | ✗ | ✗ | ✗ | ✓ | +| Priority Support | ✗ | ✗ | ✗ | ✓ | + +### License Key Format + +License keys follow the format: `IPMAN-XXXX-XXXX-XXXX-XXXX` + +The first character after `IPMAN-` determines the license tier: +- `B` - Basic license +- `P` - Professional license +- `E` - Enterprise license +- Any other character - Trial license + +### Activating a License + +1. Navigate to **Settings > License** tab +2. Enter your license key in the format `IPMAN-XXXX-XXXX-XXXX-XXXX` +3. Enter your company/licensee name +4. Enter your billing email address +5. Click **Activate License** + +The license status will update immediately showing your tier and usage limits. + +### Customer Deployment + +Each ISP customer deploys their own dedicated instance: + +1. **Provision Infrastructure** + - Deploy using Docker Compose or Dokploy + - Configure MariaDB database + - Set up Cloudflare Access for authentication (recommended) + +2. **Configure Environment** + ```env + DB_NAME=geofeed_manager + DB_USER=geofeed + DB_PASSWORD=secure_password + ADMIN_EMAILS=admin@customer-isp.com + ``` + +3. **Activate License** + - Provide the customer with their license key + - Customer activates via Settings > License tab + - License determines available features and limits + +4. **Optional Integrations** + - **Webhooks** (Basic+): Configure n8n for CDN automation + - **IP Enrichment** (Professional+): Add ipregistry.co API key + - **PTR Records** (Professional+): Configure AWS Route53 credentials + - **Whitelabel** (Professional+): Customize branding with customer logo + +### Generating License Keys + +Contact Purple Computing for license key generation: +- Email: info@purplecomputing.com +- Include: Customer name, email, desired tier + --- **Copyright (c) 2025 Purple Computing Ltd. All rights reserved.** diff --git a/database/schema.sql b/database/schema.sql index 7906c3d..aa7f654 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -214,3 +214,20 @@ ALTER TABLE geofeed_entries ADD COLUMN IF NOT EXISTS flag_threat TINYINT(1) DEFA ALTER TABLE geofeed_entries ADD INDEX IF NOT EXISTS idx_sort_order (sort_order); ALTER TABLE geofeed_entries ADD INDEX IF NOT EXISTS idx_isp (ipr_isp); ALTER TABLE geofeed_entries ADD INDEX IF NOT EXISTS idx_asn (ipr_asn); + +-- License information table +CREATE TABLE IF NOT EXISTS license_info ( + id INT AUTO_INCREMENT PRIMARY KEY, + license_key VARCHAR(64) NOT NULL, + licensee_name VARCHAR(255) NOT NULL, + licensee_email VARCHAR(255) NOT NULL, + license_type ENUM('trial', 'basic', 'professional', 'enterprise') DEFAULT 'trial', + max_entries INT DEFAULT 100, + max_users INT DEFAULT 3, + features JSON DEFAULT NULL, + issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NULL, + last_validated_at TIMESTAMP NULL, + is_active TINYINT(1) DEFAULT 1, + UNIQUE KEY unique_license (license_key) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/webapp/api.php b/webapp/api.php index fa4d750..4528589 100644 --- a/webapp/api.php +++ b/webapp/api.php @@ -5,6 +5,7 @@ */ require_once __DIR__ . '/config.php'; +require_once __DIR__ . '/includes/license.php'; header('Content-Type: application/json'); header('X-Content-Type-Options: nosniff'); @@ -251,6 +252,23 @@ try { handleAdminUserToggle($db); break; + // License management (admin only) + case 'license_status': + handleLicenseStatus($db); + break; + + case 'license_activate': + handleLicenseActivate($db); + break; + + case 'license_deactivate': + handleLicenseDeactivate($db); + break; + + case 'license_usage': + handleLicenseUsage($db); + break; + default: jsonResponse(['error' => 'Invalid action'], 400); } @@ -369,7 +387,20 @@ function handleCreate($db) { if (!validateCSRFToken($input['csrf_token'] ?? '')) { jsonResponse(['error' => 'Invalid CSRF token'], 403); } - + + // Check license entry limit + $entryCheck = canAddEntry($db); + if (!$entryCheck['allowed']) { + $max = $entryCheck['max']; + $current = $entryCheck['current']; + jsonResponse([ + 'error' => "Entry limit reached. Your license allows $max entries (currently $current). Please upgrade your license to add more entries.", + 'license_limit' => true, + 'current' => $current, + 'max' => $max + ], 403); + } + // Validate required fields if (empty($input['ip_prefix'])) { jsonResponse(['error' => 'IP prefix is required'], 400); @@ -1191,6 +1222,11 @@ function handleWebhookSettingsSave($db) { jsonResponse(['error' => 'Method not allowed'], 405); } + // Check license feature + if (!hasFeature('webhooks', $db)) { + jsonResponse(['error' => 'Webhook feature requires a Basic or higher license. Please upgrade to enable webhooks.', 'license_required' => 'webhooks'], 403); + } + $input = json_decode(file_get_contents('php://input'), true); // Validate CSRF @@ -1488,6 +1524,11 @@ function handleIpRegistrySettingsSave($db) { jsonResponse(['error' => 'Method not allowed'], 405); } + // Check license feature + if (!hasFeature('ip_enrichment', $db)) { + jsonResponse(['error' => 'IP enrichment feature requires a Professional or higher license. Please upgrade to enable IP Registry integration.', 'license_required' => 'ip_enrichment'], 403); + } + $input = json_decode(file_get_contents('php://input'), true); // Validate CSRF @@ -2400,6 +2441,11 @@ function handleAwsSettingsSave($db) { jsonResponse(['error' => 'Method not allowed'], 405); } + // Check license feature + if (!hasFeature('ptr_records', $db)) { + jsonResponse(['error' => 'PTR record management feature requires a Professional or higher license. Please upgrade to enable AWS Route53 integration.', 'license_required' => 'ptr_records'], 403); + } + $input = json_decode(file_get_contents('php://input'), true); if (!validateCSRFToken($input['csrf_token'] ?? '')) { @@ -3208,6 +3254,11 @@ function handleWhitelabelSave($db) { jsonResponse(['error' => 'Method not allowed'], 405); } + // Check license feature + if (!hasFeature('whitelabel', $db)) { + jsonResponse(['error' => 'Whitelabel feature requires a Professional or higher license. Please upgrade to customize branding.', 'license_required' => 'whitelabel'], 403); + } + $input = json_decode(file_get_contents('php://input'), true); // Verify CSRF token @@ -3406,6 +3457,27 @@ function handleAdminUserSave($db) { $displayName = $input['display_name'] ?? null; $createdBy = getAuditUser(); + // Check if this is a new user (not an update) + $existingUser = $db->prepare("SELECT id FROM admin_users WHERE email = :email"); + $existingUser->execute([':email' => $email]); + $isNewUser = !$existingUser->fetch(); + + // Check license user limit for new users + if ($isNewUser) { + $userCheck = canAddUser($db); + if (!$userCheck['allowed']) { + $max = $userCheck['max']; + $current = $userCheck['current']; + jsonResponse([ + 'error' => "User limit reached. Your license allows $max users (currently $current). Please upgrade your license to add more users.", + 'license_limit' => true, + 'current' => $current, + 'max' => $max + ], 403); + return; + } + } + $success = saveAdminUser($email, $role, $displayName, $createdBy); if ($success) { @@ -3500,3 +3572,102 @@ function handleAdminUserToggle($db) { jsonResponse(['error' => 'Failed to toggle user status'], 500); } } + +/** + * Get license status + */ +function handleLicenseStatus($db) { + $status = getLicenseStatus($db); + jsonResponse([ + 'success' => true, + 'license' => $status + ]); +} + +/** + * Activate a license key (admin only) + */ +function handleLicenseActivate($db) { + require_once __DIR__ . '/includes/auth.php'; + + if (!isAdmin()) { + jsonResponse(['error' => 'Admin access required'], 403); + return; + } + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + jsonResponse(['error' => 'POST method required'], 405); + return; + } + + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['license_key'])) { + jsonResponse(['error' => 'License key is required'], 400); + return; + } + + if (empty($input['licensee_name'])) { + jsonResponse(['error' => 'Licensee name is required'], 400); + return; + } + + if (empty($input['licensee_email'])) { + jsonResponse(['error' => 'Licensee email is required'], 400); + return; + } + + $result = activateLicense( + $db, + $input['license_key'], + $input['licensee_name'], + $input['licensee_email'] + ); + + if ($result['success']) { + jsonResponse([ + 'success' => true, + 'message' => $result['message'], + 'type' => $result['type'], + 'expires_at' => $result['expires_at'] + ]); + } else { + jsonResponse(['error' => $result['error']], 400); + } +} + +/** + * Deactivate the current license (admin only) + */ +function handleLicenseDeactivate($db) { + require_once __DIR__ . '/includes/auth.php'; + + if (!isAdmin()) { + jsonResponse(['error' => 'Admin access required'], 403); + return; + } + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + jsonResponse(['error' => 'POST method required'], 405); + return; + } + + $result = deactivateLicense($db); + + if ($result['success']) { + jsonResponse(['success' => true, 'message' => $result['message']]); + } else { + jsonResponse(['error' => $result['error']], 500); + } +} + +/** + * Get license usage statistics + */ +function handleLicenseUsage($db) { + $usage = getLicenseUsage($db); + jsonResponse([ + 'success' => true, + 'usage' => $usage + ]); +} diff --git a/webapp/includes/license.php b/webapp/includes/license.php new file mode 100644 index 0000000..5f214e2 --- /dev/null +++ b/webapp/includes/license.php @@ -0,0 +1,405 @@ + [ + 'max_entries' => 100, + 'max_users' => 2, + 'features' => ['basic_crud', 'csv_export'], + 'name' => 'Trial', + 'duration_days' => 14 + ], + LICENSE_BASIC => [ + 'max_entries' => 500, + 'max_users' => 5, + 'features' => ['basic_crud', 'csv_export', 'webhooks', 'audit_log'], + 'name' => 'Basic' + ], + LICENSE_PROFESSIONAL => [ + 'max_entries' => 2500, + 'max_users' => 15, + 'features' => ['basic_crud', 'csv_export', 'webhooks', 'audit_log', 'ip_enrichment', 'whitelabel', 'ptr_records'], + 'name' => 'Professional' + ], + LICENSE_ENTERPRISE => [ + 'max_entries' => -1, // Unlimited + 'max_users' => -1, // Unlimited + 'features' => ['basic_crud', 'csv_export', 'webhooks', 'audit_log', 'ip_enrichment', 'whitelabel', 'ptr_records', 'api_access', 'priority_support'], + 'name' => 'Enterprise' + ] +]; + +/** + * Generate a new license key + * Format: IPMAN-XXXX-XXXX-XXXX-XXXX + */ +function generateLicenseKey() { + $segments = []; + for ($i = 0; $i < 4; $i++) { + $segments[] = strtoupper(bin2hex(random_bytes(2))); + } + return 'IPMAN-' . implode('-', $segments); +} + +/** + * Create a signed license key with embedded data + */ +function createSignedLicense($licenseeEmail, $licenseType, $expiresAt = null) { + $data = [ + 'e' => $licenseeEmail, + 't' => $licenseType, + 'i' => time(), + 'x' => $expiresAt ? strtotime($expiresAt) : null + ]; + + $payload = base64_encode(json_encode($data)); + $signature = hash_hmac('sha256', $payload, getLicenseSecret()); + + return 'IPMAN-' . substr($signature, 0, 8) . '-' . chunk_split(strtoupper(bin2hex(random_bytes(6))), 4, '-'); +} + +/** + * Get license secret (from environment or generate) + */ +function getLicenseSecret() { + $secret = getenv('LICENSE_SECRET'); + if (empty($secret)) { + $secret = 'purple-computing-isp-ip-manager-2025'; + } + return $secret; +} + +/** + * Validate a license key format + */ +function isValidLicenseFormat($key) { + return preg_match('/^IPMAN-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/i', $key); +} + +/** + * Get current license info from database + */ +function getLicenseInfo($db = null) { + if (!$db && function_exists('getDB')) { + $db = getDB(); + } + + if (!$db) { + return null; + } + + try { + $stmt = $db->query("SELECT * FROM license_info WHERE is_active = 1 ORDER BY id DESC LIMIT 1"); + return $stmt->fetch(PDO::FETCH_ASSOC); + } catch (Exception $e) { + return null; + } +} + +/** + * Check if license is valid and not expired + */ +function isLicenseValid($db = null) { + $license = getLicenseInfo($db); + + if (!$license) { + return false; + } + + // Check if expired + if ($license['expires_at'] && strtotime($license['expires_at']) < time()) { + return false; + } + + return $license['is_active'] == 1; +} + +/** + * Get license status with details + */ +function getLicenseStatus($db = null) { + global $LICENSE_LIMITS; + + $license = getLicenseInfo($db); + + if (!$license) { + return [ + 'valid' => false, + 'status' => 'unlicensed', + 'message' => 'No license key configured', + 'type' => null, + 'limits' => $LICENSE_LIMITS[LICENSE_TRIAL] + ]; + } + + $isExpired = $license['expires_at'] && strtotime($license['expires_at']) < time(); + + if ($isExpired) { + return [ + 'valid' => false, + 'status' => 'expired', + 'message' => 'License expired on ' . date('Y-m-d', strtotime($license['expires_at'])), + 'type' => $license['license_type'], + 'limits' => $LICENSE_LIMITS[LICENSE_TRIAL], + 'license' => $license + ]; + } + + if (!$license['is_active']) { + return [ + 'valid' => false, + 'status' => 'inactive', + 'message' => 'License has been deactivated', + 'type' => $license['license_type'], + 'limits' => $LICENSE_LIMITS[LICENSE_TRIAL], + 'license' => $license + ]; + } + + $type = $license['license_type'] ?: LICENSE_TRIAL; + $limits = $LICENSE_LIMITS[$type] ?? $LICENSE_LIMITS[LICENSE_TRIAL]; + + // Override with custom limits if set + if ($license['max_entries']) { + $limits['max_entries'] = $license['max_entries']; + } + if ($license['max_users']) { + $limits['max_users'] = $license['max_users']; + } + if ($license['features']) { + $customFeatures = json_decode($license['features'], true); + if (is_array($customFeatures)) { + $limits['features'] = $customFeatures; + } + } + + $daysRemaining = null; + if ($license['expires_at']) { + $daysRemaining = max(0, floor((strtotime($license['expires_at']) - time()) / 86400)); + } + + return [ + 'valid' => true, + 'status' => 'active', + 'message' => $limits['name'] . ' License', + 'type' => $type, + 'limits' => $limits, + 'license' => $license, + 'days_remaining' => $daysRemaining, + 'expires_at' => $license['expires_at'] + ]; +} + +/** + * Check if a specific feature is available + */ +function hasFeature($feature, $db = null) { + $status = getLicenseStatus($db); + + // Always allow basic features for unlicensed/trial + $basicFeatures = ['basic_crud', 'csv_export']; + if (in_array($feature, $basicFeatures)) { + return true; + } + + if (!$status['valid']) { + return false; + } + + return in_array($feature, $status['limits']['features']); +} + +/** + * Check if entry limit has been reached + */ +function canAddEntry($db = null) { + if (!$db && function_exists('getDB')) { + $db = getDB(); + } + + $status = getLicenseStatus($db); + $maxEntries = $status['limits']['max_entries']; + + // -1 means unlimited + if ($maxEntries == -1) { + return ['allowed' => true, 'current' => 0, 'max' => -1]; + } + + try { + $stmt = $db->query("SELECT COUNT(*) FROM geofeed_entries"); + $current = (int)$stmt->fetchColumn(); + + return [ + 'allowed' => $current < $maxEntries, + 'current' => $current, + 'max' => $maxEntries + ]; + } catch (Exception $e) { + return ['allowed' => true, 'current' => 0, 'max' => $maxEntries]; + } +} + +/** + * Check if user limit has been reached + */ +function canAddUser($db = null) { + if (!$db && function_exists('getDB')) { + $db = getDB(); + } + + $status = getLicenseStatus($db); + $maxUsers = $status['limits']['max_users']; + + // -1 means unlimited + if ($maxUsers == -1) { + return ['allowed' => true, 'current' => 0, 'max' => -1]; + } + + try { + $stmt = $db->query("SELECT COUNT(*) FROM admin_users WHERE active = 1"); + $current = (int)$stmt->fetchColumn(); + + return [ + 'allowed' => $current < $maxUsers, + 'current' => $current, + 'max' => $maxUsers + ]; + } catch (Exception $e) { + return ['allowed' => true, 'current' => 0, 'max' => $maxUsers]; + } +} + +/** + * Activate a license key + */ +function activateLicense($db, $licenseKey, $licenseeName, $licenseeEmail) { + if (!isValidLicenseFormat($licenseKey)) { + return ['success' => false, 'error' => 'Invalid license key format']; + } + + // For now, accept any valid format key as a trial + // In production, you would validate against a license server + $licenseType = LICENSE_TRIAL; + $expiresAt = date('Y-m-d H:i:s', strtotime('+14 days')); + + // Check if key starts with specific prefixes for different tiers + if (preg_match('/^IPMAN-[0-9A-F]{4}/', $licenseKey)) { + $prefix = substr($licenseKey, 6, 1); + switch ($prefix) { + case 'B': + $licenseType = LICENSE_BASIC; + $expiresAt = date('Y-m-d H:i:s', strtotime('+1 year')); + break; + case 'P': + $licenseType = LICENSE_PROFESSIONAL; + $expiresAt = date('Y-m-d H:i:s', strtotime('+1 year')); + break; + case 'E': + $licenseType = LICENSE_ENTERPRISE; + $expiresAt = null; // No expiry + break; + } + } + + global $LICENSE_LIMITS; + $limits = $LICENSE_LIMITS[$licenseType]; + + try { + // Deactivate any existing licenses + $db->exec("UPDATE license_info SET is_active = 0"); + + // Insert new license + $stmt = $db->prepare(" + INSERT INTO license_info (license_key, licensee_name, licensee_email, license_type, max_entries, max_users, features, expires_at, is_active) + VALUES (:key, :name, :email, :type, :max_entries, :max_users, :features, :expires, 1) + "); + + $stmt->execute([ + ':key' => $licenseKey, + ':name' => $licenseeName, + ':email' => $licenseeEmail, + ':type' => $licenseType, + ':max_entries' => $limits['max_entries'], + ':max_users' => $limits['max_users'], + ':features' => json_encode($limits['features']), + ':expires' => $expiresAt + ]); + + return [ + 'success' => true, + 'message' => 'License activated successfully', + 'type' => $licenseType, + 'expires_at' => $expiresAt + ]; + } catch (Exception $e) { + return ['success' => false, 'error' => 'Failed to activate license: ' . $e->getMessage()]; + } +} + +/** + * Deactivate current license + */ +function deactivateLicense($db) { + try { + $db->exec("UPDATE license_info SET is_active = 0"); + return ['success' => true, 'message' => 'License deactivated']; + } catch (Exception $e) { + return ['success' => false, 'error' => 'Failed to deactivate license']; + } +} + +/** + * Get usage statistics for license + */ +function getLicenseUsage($db = null) { + if (!$db && function_exists('getDB')) { + $db = getDB(); + } + + $status = getLicenseStatus($db); + + $entryCount = 0; + $userCount = 0; + + try { + $stmt = $db->query("SELECT COUNT(*) FROM geofeed_entries"); + $entryCount = (int)$stmt->fetchColumn(); + + $stmt = $db->query("SELECT COUNT(*) FROM admin_users WHERE active = 1"); + $userCount = (int)$stmt->fetchColumn(); + } catch (Exception $e) { + // Ignore + } + + $maxEntries = $status['limits']['max_entries']; + $maxUsers = $status['limits']['max_users']; + + return [ + 'entries' => [ + 'current' => $entryCount, + 'max' => $maxEntries, + 'percentage' => $maxEntries > 0 ? round(($entryCount / $maxEntries) * 100) : 0, + 'unlimited' => $maxEntries == -1 + ], + 'users' => [ + 'current' => $userCount, + 'max' => $maxUsers, + 'percentage' => $maxUsers > 0 ? round(($userCount / $maxUsers) * 100) : 0, + 'unlimited' => $maxUsers == -1 + ], + 'license' => $status + ]; +} diff --git a/webapp/settings.php b/webapp/settings.php index 5d2f119..708c76e 100644 --- a/webapp/settings.php +++ b/webapp/settings.php @@ -45,7 +45,7 @@ $showSettingsLink = false; // Get current tab from URL $currentTab = $_GET['tab'] ?? 'integrations'; -$validTabs = ['integrations', 'users', 'audit', 'advanced', 'whitelabel', 'developer']; +$validTabs = ['integrations', 'users', 'audit', 'advanced', 'whitelabel', 'license', 'developer']; if (!in_array($currentTab, $validTabs)) { $currentTab = 'integrations'; } @@ -106,6 +106,13 @@ require_once __DIR__ . '/includes/header.php'; Whitelabel + + + + + + License + @@ -527,6 +534,151 @@ require_once __DIR__ . '/includes/header.php'; + +
+ +
+

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

+
+
+
@@ -643,6 +795,10 @@ document.addEventListener("DOMContentLoaded", function() { case "whitelabel": loadWhitelabelSettings(); break; + case "license": + loadLicenseStatus(); + loadLicenseUsage(); + break; case "developer": loadSystemInfo(); loadErrorLogs(); @@ -820,6 +976,254 @@ function formatDate(dateStr) { 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"); + } +} '; // Include footer