Files
ip-manager/webapp/includes/license.php
Purple 753661a34c 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>
2026-01-18 13:21:44 +00:00

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
];
}