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:
Purple
2026-02-10 17:45:23 +00:00
parent 336121d758
commit ebfbf27b9c
3 changed files with 181 additions and 45 deletions

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -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();