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>
This commit is contained in:
@@ -289,6 +289,10 @@ try {
|
||||
handleLicenseUsage($db);
|
||||
break;
|
||||
|
||||
case 'license_generate':
|
||||
handleLicenseGenerate($db);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => 'Invalid action'], 400);
|
||||
}
|
||||
@@ -3717,6 +3721,49 @@ function handleLicenseUsage($db) {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate sample license keys (admin only, for testing)
|
||||
*/
|
||||
function handleLicenseGenerate($db) {
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
|
||||
if (!isAdmin()) {
|
||||
jsonResponse(['error' => 'Admin access required'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
jsonResponse(['error' => 'GET method required'], 405);
|
||||
return;
|
||||
}
|
||||
|
||||
$tier = $_GET['tier'] ?? 'trial';
|
||||
|
||||
// Map tier name to constant
|
||||
$tierMap = [
|
||||
'trial' => LICENSE_TRIAL,
|
||||
'basic' => LICENSE_BASIC,
|
||||
'professional' => LICENSE_PROFESSIONAL,
|
||||
'enterprise' => LICENSE_ENTERPRISE
|
||||
];
|
||||
|
||||
$tierConst = $tierMap[$tier] ?? LICENSE_TRIAL;
|
||||
$key = generateLicenseKey($tierConst);
|
||||
|
||||
global $LICENSE_LIMITS;
|
||||
$limits = $LICENSE_LIMITS[$tierConst];
|
||||
|
||||
jsonResponse([
|
||||
'success' => true,
|
||||
'license_key' => $key,
|
||||
'tier' => $tier,
|
||||
'tier_name' => $limits['name'],
|
||||
'max_entries' => $limits['max_entries'],
|
||||
'max_users' => $limits['max_users'],
|
||||
'features' => $limits['features']
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export full database backup as JSON
|
||||
*/
|
||||
|
||||
@@ -43,34 +43,37 @@ $LICENSE_LIMITS = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Generate a new license key
|
||||
* Format: IPMAN-XXXX-XXXX-XXXX-XXXX
|
||||
* Tier prefix mapping for license keys
|
||||
* First segment encodes the tier: Txxxx, Bxxxx, Pxxxx, Exxxx
|
||||
*/
|
||||
function generateLicenseKey() {
|
||||
$segments = [];
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
@@ -84,9 +87,25 @@ function getLicenseSecret() {
|
||||
|
||||
/**
|
||||
* Validate a license key format
|
||||
* Accepts: IPMAN-XXXX-XXXX-XXXX-XXXX where X is alphanumeric hex
|
||||
*/
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,31 +306,26 @@ function canAddUser($db = null) {
|
||||
*/
|
||||
function activateLicense($db, $licenseKey, $licenseeName, $licenseeEmail) {
|
||||
if (!isValidLicenseFormat($licenseKey)) {
|
||||
return ['success' => false, 'error' => 'Invalid license key format'];
|
||||
return ['success' => false, 'error' => 'Invalid license key format. Expected: IPMAN-XXXX-XXXX-XXXX-XXXX'];
|
||||
}
|
||||
|
||||
// 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'));
|
||||
// Detect tier from key prefix
|
||||
$licenseType = detectLicenseTier($licenseKey);
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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;
|
||||
|
||||
@@ -612,6 +612,44 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate Sample Keys (for testing) -->
|
||||
<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="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/>
|
||||
</svg>
|
||||
Generate Sample License Key
|
||||
</h2>
|
||||
<p class="advanced-section-desc">Generate sample license keys for testing. Keys start with T (Trial), B (Basic), P (Professional), or E (Enterprise).</p>
|
||||
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px;">
|
||||
<select id="generateTier" class="form-input" style="width: auto; min-width: 150px;">
|
||||
<option value="trial">Trial</option>
|
||||
<option value="basic">Basic</option>
|
||||
<option value="professional">Professional</option>
|
||||
<option value="enterprise">Enterprise</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" onclick="generateSampleKey()">
|
||||
<svg width="16" height="16" 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>
|
||||
Generate Key
|
||||
</button>
|
||||
</div>
|
||||
<div id="generatedKeyContainer" style="margin-top: 12px; display: none;">
|
||||
<div style="padding: 12px; background: var(--bg-tertiary); border-radius: var(--radius-md); font-family: monospace; display: flex; align-items: center; gap: 12px;">
|
||||
<span id="generatedKey" style="flex: 1;"></span>
|
||||
<button class="btn btn-sm" onclick="copyGeneratedKey()" title="Copy to clipboard">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="useGeneratedKey()" title="Use this key">Use</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License Tiers -->
|
||||
<div class="advanced-section">
|
||||
<h2 class="advanced-section-title">
|
||||
@@ -1267,6 +1305,43 @@ function renderUsageBar(usage) {
|
||||
`;
|
||||
}
|
||||
|
||||
async function generateSampleKey() {
|
||||
const tier = document.getElementById("generateTier").value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`api.php?action=license_generate&tier=${tier}`, {
|
||||
credentials: "same-origin"
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || "Failed to generate key");
|
||||
}
|
||||
|
||||
document.getElementById("generatedKey").textContent = data.license_key;
|
||||
document.getElementById("generatedKeyContainer").style.display = "block";
|
||||
showNotification(`Generated ${data.tier_name} license key`, "success");
|
||||
} catch (error) {
|
||||
showNotification("Failed to generate key: " + error.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function copyGeneratedKey() {
|
||||
const key = document.getElementById("generatedKey").textContent;
|
||||
navigator.clipboard.writeText(key).then(() => {
|
||||
showNotification("License key copied to clipboard", "success");
|
||||
}).catch(() => {
|
||||
showNotification("Failed to copy to clipboard", "error");
|
||||
});
|
||||
}
|
||||
|
||||
function useGeneratedKey() {
|
||||
const key = document.getElementById("generatedKey").textContent;
|
||||
document.getElementById("licenseKey").value = key;
|
||||
document.getElementById("generatedKeyContainer").style.display = "none";
|
||||
showNotification("Key copied to activation form", "info");
|
||||
}
|
||||
|
||||
async function activateLicense() {
|
||||
const licenseKey = document.getElementById("licenseKey").value.trim();
|
||||
const licenseeName = document.getElementById("licenseeName").value.trim();
|
||||
|
||||
Reference in New Issue
Block a user