Add licensing system for ISP customers
- Add license.php with tiered licensing (Trial, Basic, Professional, Enterprise) - Add license_info table to database schema - Add license management UI to settings (License tab) - Add license status, activation, and usage API endpoints - Add entry and user limit enforcement based on license tier - Add feature flags for webhooks, IP enrichment, whitelabel, PTR records - Update README with licensing documentation and customer deployment guide Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
75
README.md
75
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.**
|
||||
|
||||
@@ -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;
|
||||
|
||||
173
webapp/api.php
173
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
|
||||
]);
|
||||
}
|
||||
|
||||
405
webapp/includes/license.php
Normal file
405
webapp/includes/license.php
Normal file
@@ -0,0 +1,405 @@
|
||||
<?php
|
||||
/**
|
||||
* License Management System
|
||||
*
|
||||
* Handles license validation, feature flags, and usage limits for ISP IP Manager
|
||||
*
|
||||
* Copyright (c) 2025 Purple Computing Ltd. All rights reserved.
|
||||
*/
|
||||
|
||||
// License tier definitions
|
||||
define('LICENSE_TRIAL', 'trial');
|
||||
define('LICENSE_BASIC', 'basic');
|
||||
define('LICENSE_PROFESSIONAL', 'professional');
|
||||
define('LICENSE_ENTERPRISE', 'enterprise');
|
||||
|
||||
// License limits by tier
|
||||
$LICENSE_LIMITS = [
|
||||
LICENSE_TRIAL => [
|
||||
'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
|
||||
];
|
||||
}
|
||||
@@ -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';
|
||||
</svg>
|
||||
Whitelabel
|
||||
</a>
|
||||
<a href="?tab=license" class="settings-nav-item <?php echo $currentTab === 'license' ? 'active' : ''; ?>">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
License
|
||||
</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"/>
|
||||
@@ -527,6 +534,151 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License Section -->
|
||||
<div class="settings-section <?php echo $currentTab === 'license' ? 'active' : ''; ?>" id="section-license">
|
||||
<!-- Current License Status -->
|
||||
<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="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
License Status
|
||||
</h2>
|
||||
<p class="advanced-section-desc">View and manage your ISP IP Manager license.</p>
|
||||
|
||||
<div id="licenseStatusContainer" style="margin-top: 16px;">
|
||||
<div class="loading"><div class="spinner"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Statistics -->
|
||||
<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 12V7H5a2 2 0 0 1 0-4h14v4"/>
|
||||
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
|
||||
<path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/>
|
||||
</svg>
|
||||
Usage Statistics
|
||||
</h2>
|
||||
<p class="advanced-section-desc">Current usage against your license limits.</p>
|
||||
|
||||
<div id="licenseUsageContainer" style="margin-top: 16px;">
|
||||
<div class="loading"><div class="spinner"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activate License -->
|
||||
<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 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
|
||||
</svg>
|
||||
Activate License
|
||||
</h2>
|
||||
<p class="advanced-section-desc">Enter your license key to activate or upgrade your plan.</p>
|
||||
|
||||
<div class="form-grid" style="margin-top: 16px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">License Key</label>
|
||||
<input type="text" class="form-input" id="licenseKey" placeholder="IPMAN-XXXX-XXXX-XXXX-XXXX" style="font-family: monospace;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Licensee Name</label>
|
||||
<input type="text" class="form-input" id="licenseeName" placeholder="Company or Individual Name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Licensee Email</label>
|
||||
<input type="email" class="form-input" id="licenseeEmail" placeholder="billing@example.com" style="max-width: 400px;">
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; margin-top: 16px;">
|
||||
<button class="btn btn-primary" onclick="activateLicense()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
Activate License
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="deactivateLicense()" id="deactivateLicenseBtn" style="display: none;">
|
||||
<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="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
Deactivate License
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License Tiers -->
|
||||
<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">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
||||
</svg>
|
||||
Available Plans
|
||||
</h2>
|
||||
<p class="advanced-section-desc">Compare features across different license tiers.</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-top: 16px;">
|
||||
<!-- Trial -->
|
||||
<div class="license-tier-card" style="padding: 20px; background: var(--bg-tertiary); border-radius: var(--radius-md); border: 1px solid var(--border-default);">
|
||||
<h3 style="font-size: 16px; font-weight: 600; margin-bottom: 8px;">Trial</h3>
|
||||
<p style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 12px;">14 days free</p>
|
||||
<ul style="font-size: 13px; color: var(--text-secondary); list-style: none; padding: 0; margin: 0;">
|
||||
<li style="margin-bottom: 6px;">✓ 100 entries</li>
|
||||
<li style="margin-bottom: 6px;">✓ 2 users</li>
|
||||
<li style="margin-bottom: 6px;">✓ Basic CRUD</li>
|
||||
<li style="margin-bottom: 6px;">✓ CSV export</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Basic -->
|
||||
<div class="license-tier-card" style="padding: 20px; background: var(--bg-tertiary); border-radius: var(--radius-md); border: 1px solid var(--border-default);">
|
||||
<h3 style="font-size: 16px; font-weight: 600; margin-bottom: 8px;">Basic</h3>
|
||||
<p style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 12px;">Small ISPs</p>
|
||||
<ul style="font-size: 13px; color: var(--text-secondary); list-style: none; padding: 0; margin: 0;">
|
||||
<li style="margin-bottom: 6px;">✓ 500 entries</li>
|
||||
<li style="margin-bottom: 6px;">✓ 5 users</li>
|
||||
<li style="margin-bottom: 6px;">✓ Webhooks</li>
|
||||
<li style="margin-bottom: 6px;">✓ Audit log</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Professional -->
|
||||
<div class="license-tier-card" style="padding: 20px; background: var(--bg-tertiary); border-radius: var(--radius-md); border: 1px solid var(--purple-primary); box-shadow: 0 0 0 1px var(--purple-primary);">
|
||||
<h3 style="font-size: 16px; font-weight: 600; margin-bottom: 8px; color: var(--purple-primary);">Professional</h3>
|
||||
<p style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 12px;">Growing ISPs</p>
|
||||
<ul style="font-size: 13px; color: var(--text-secondary); list-style: none; padding: 0; margin: 0;">
|
||||
<li style="margin-bottom: 6px;">✓ 2,500 entries</li>
|
||||
<li style="margin-bottom: 6px;">✓ 15 users</li>
|
||||
<li style="margin-bottom: 6px;">✓ IP enrichment</li>
|
||||
<li style="margin-bottom: 6px;">✓ Whitelabel</li>
|
||||
<li style="margin-bottom: 6px;">✓ PTR records</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Enterprise -->
|
||||
<div class="license-tier-card" style="padding: 20px; background: var(--bg-tertiary); border-radius: var(--radius-md); border: 1px solid var(--border-default);">
|
||||
<h3 style="font-size: 16px; font-weight: 600; margin-bottom: 8px;">Enterprise</h3>
|
||||
<p style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 12px;">Large ISPs</p>
|
||||
<ul style="font-size: 13px; color: var(--text-secondary); list-style: none; padding: 0; margin: 0;">
|
||||
<li style="margin-bottom: 6px;">✓ Unlimited entries</li>
|
||||
<li style="margin-bottom: 6px;">✓ Unlimited users</li>
|
||||
<li style="margin-bottom: 6px;">✓ API access</li>
|
||||
<li style="margin-bottom: 6px;">✓ Priority support</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 16px; font-size: 13px; color: var(--text-tertiary);">
|
||||
Contact <a href="mailto:info@purplecomputing.com" style="color: var(--purple-primary);">info@purplecomputing.com</a> for pricing and license keys.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Developer Section -->
|
||||
<div class="settings-section <?php echo $currentTab === 'developer' ? 'active' : ''; ?>" id="section-developer">
|
||||
<!-- Import Section -->
|
||||
@@ -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 = `<p style="margin-top: 8px; font-size: 13px; color: var(--text-tertiary);">
|
||||
Expires: ${expiryDate.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })}
|
||||
${license.days_remaining !== null ? `(${license.days_remaining} days remaining)` : ""}
|
||||
</p>`;
|
||||
}
|
||||
|
||||
let licenseDetails = "";
|
||||
if (license.license && license.license.licensee_name) {
|
||||
licenseDetails = `
|
||||
<div style="margin-top: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: var(--radius-md);">
|
||||
<p style="font-size: 13px; margin-bottom: 4px;"><strong>Licensee:</strong> ${escapeHtml(license.license.licensee_name)}</p>
|
||||
<p style="font-size: 13px; margin-bottom: 4px;"><strong>Email:</strong> ${escapeHtml(license.license.licensee_email)}</p>
|
||||
<p style="font-size: 13px; margin-bottom: 0;"><strong>License Key:</strong> <code style="font-family: monospace; background: var(--bg-secondary); padding: 2px 6px; border-radius: 4px;">${maskLicenseKey(license.license.license_key)}</code></p>
|
||||
</div>
|
||||
`;
|
||||
// Show deactivate button
|
||||
document.getElementById("deactivateLicenseBtn").style.display = "inline-flex";
|
||||
} else {
|
||||
document.getElementById("deactivateLicenseBtn").style.display = "none";
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
|
||||
<span class="badge ${statusClass}" style="font-size: 14px; padding: 6px 12px;">${statusBadge}</span>
|
||||
<span style="font-size: 16px; font-weight: 600;">${license.limits?.name || "Unknown"} License</span>
|
||||
</div>
|
||||
<p style="color: var(--text-secondary);">${license.message}</p>
|
||||
${expiryInfo}
|
||||
${licenseDetails}
|
||||
`;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading license status:", error);
|
||||
document.getElementById("licenseStatusContainer").innerHTML = `
|
||||
<div style="color: var(--error); padding: 12px; background: var(--error-bg); border-radius: var(--radius-md);">
|
||||
Failed to load license status: ${escapeHtml(error.message)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px;">
|
||||
<div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
||||
<span style="font-weight: 500;">Geofeed Entries</span>
|
||||
<span style="color: var(--text-tertiary);">
|
||||
${usage.entries.current} / ${usage.entries.unlimited ? "Unlimited" : usage.entries.max}
|
||||
</span>
|
||||
</div>
|
||||
${entriesBar}
|
||||
</div>
|
||||
<div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
||||
<span style="font-weight: 500;">Users</span>
|
||||
<span style="color: var(--text-tertiary);">
|
||||
${usage.users.current} / ${usage.users.unlimited ? "Unlimited" : usage.users.max}
|
||||
</span>
|
||||
</div>
|
||||
${usersBar}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading license usage:", error);
|
||||
document.getElementById("licenseUsageContainer").innerHTML = `
|
||||
<div style="color: var(--error); padding: 12px; background: var(--error-bg); border-radius: var(--radius-md);">
|
||||
Failed to load usage data: ${escapeHtml(error.message)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderUsageBar(usage) {
|
||||
if (usage.unlimited) {
|
||||
return `<div style="height: 8px; background: var(--purple-primary); border-radius: 4px;"></div>`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div style="height: 8px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden;">
|
||||
<div style="height: 100%; width: ${percentage}%; background: ${barColor}; border-radius: 4px; transition: width 0.3s;"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
</script>';
|
||||
|
||||
// Include footer
|
||||
|
||||
Reference in New Issue
Block a user