Files
ip-manager/webapp/includes/license.php
Purple ebfbf27b9c Fix license system - tier-aware key generation and activation
- generateLicenseKey() now accepts tier parameter and prefixes keys:
  T=Trial, B=Basic, P=Professional, E=Enterprise
- Added detectLicenseTier() to extract tier from key prefix
- Updated isValidLicenseFormat() regex to accept tier prefixes
- Simplified activateLicense() to use detectLicenseTier()
- Added license_generate API endpoint for generating sample keys
- Added "Generate Sample License Key" UI section with tier selector
- Added copy/use buttons for generated keys

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 17:45:23 +00:00

420 lines
12 KiB
PHP

<?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'
]
];
/**
* Tier prefix mapping for license keys
* First segment encodes the tier: Txxxx, Bxxxx, Pxxxx, Exxxx
*/
define('LICENSE_TIER_PREFIXES', [
LICENSE_TRIAL => 'T',
LICENSE_BASIC => 'B',
LICENSE_PROFESSIONAL => 'P',
LICENSE_ENTERPRISE => 'E'
]);
/**
* Generate a new license key for a given tier
* Format: IPMAN-TYYY-XXXX-XXXX-XXXX
* Where T is the tier prefix (T=Trial, B=Basic, P=Professional, E=Enterprise)
* and the rest are random hex characters
*/
function generateLicenseKey($tier = LICENSE_TRIAL) {
$prefixes = LICENSE_TIER_PREFIXES;
$prefix = $prefixes[$tier] ?? 'T';
// First segment: tier prefix + 3 random hex chars
$seg1 = $prefix . strtoupper(bin2hex(random_bytes(1))) . strtoupper(dechex(random_int(0, 15)));
// Remaining segments: random hex
$segments = [$seg1];
for ($i = 0; $i < 3; $i++) {
$segments[] = strtoupper(bin2hex(random_bytes(2)));
}
return 'IPMAN-' . implode('-', $segments);
}
/**
* 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
* Accepts: IPMAN-XXXX-XXXX-XXXX-XXXX where X is alphanumeric hex
*/
function isValidLicenseFormat($key) {
return preg_match('/^IPMAN-[A-Z0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/i', $key);
}
/**
* Detect license tier from a license key
*/
function detectLicenseTier($key) {
if (!isValidLicenseFormat($key)) {
return LICENSE_TRIAL;
}
// First char of first segment after IPMAN-
$tierChar = strtoupper(substr($key, 6, 1));
$prefixes = array_flip(LICENSE_TIER_PREFIXES);
return $prefixes[$tierChar] ?? LICENSE_TRIAL;
}
/**
* 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. Expected: IPMAN-XXXX-XXXX-XXXX-XXXX'];
}
// Detect tier from key prefix
$licenseType = detectLicenseTier($licenseKey);
// Set expiry based on tier
switch ($licenseType) {
case LICENSE_BASIC:
$expiresAt = date('Y-m-d H:i:s', strtotime('+1 year'));
break;
case LICENSE_PROFESSIONAL:
$expiresAt = date('Y-m-d H:i:s', strtotime('+1 year'));
break;
case LICENSE_ENTERPRISE:
$expiresAt = null; // No expiry
break;
default: // Trial
$expiresAt = date('Y-m-d H:i:s', strtotime('+14 days'));
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
];
}