- 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>
406 lines
11 KiB
PHP
406 lines
11 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'
|
|
]
|
|
];
|
|
|
|
/**
|
|
* 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
|
|
];
|
|
}
|