getMessage(); } if (isset($_GET['logout'])) { unset($_SESSION['clm_admin_auth']); clm_redirect('manage.php'); } $error = $schemaError; if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'login') { if ($schemaError !== '') { $error = $schemaError; } elseif (hash_equals((string)clm_cfg('admin_password'), (string)($_POST['password'] ?? ''))) { $_SESSION['clm_admin_auth'] = true; clm_redirect('manage.php'); } else { $error = 'Wrong admin password.'; } } $isAuthenticated = !empty($_SESSION['clm_admin_auth']); if ($isAuthenticated && $schemaError === '' && $_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') !== 'login') { try { $action = (string)($_POST['action'] ?? ''); $tables = clm_tables(); $activationsTable = $tables['activations']; switch ($action) { case 'create_app': $name = trim((string)($_POST['name'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); if ($name === '' && $slug === '') { throw new RuntimeException('App name is required.'); } $app = clm_resolve_app($slug, $name, true); if (!$app) { throw new RuntimeException('App could not be created.'); } clm_set_flash('success', 'App saved: ' . $app['name']); break; case 'save_app': $app = clm_app_by_id((int)($_POST['id'] ?? 0)); if (!$app) { throw new RuntimeException('App not found.'); } $app = clm_update_app_record($app, [ 'name' => $_POST['name'] ?? '', 'slug' => $_POST['slug'] ?? '', 'status' => $_POST['status'] ?? 'active', ]); clm_set_flash('success', 'App updated: ' . $app['name']); break; case 'issue_license': $license = clm_issue_license_record([ 'app_slug' => $_POST['app_slug'] ?? '', 'app_name' => $_POST['app_name'] ?? '', 'prefix' => $_POST['prefix'] ?? 'FLAT', 'max_activations' => $_POST['max_activations'] ?? 1, 'max_counters' => $_POST['max_counters'] ?? 1, 'owner' => $_POST['owner'] ?? '', 'customer_name' => $_POST['customer_name'] ?? '', 'customer_email' => $_POST['customer_email'] ?? '', 'address' => $_POST['address'] ?? '', 'expires_at' => $_POST['expires_at'] ?? '', 'notes' => $_POST['notes'] ?? '', ]); clm_set_flash('success', 'License issued: ' . $license['license_key']); break; case 'save_license': $license = clm_license_with_app_by_id((int)($_POST['id'] ?? 0)); if (!$license) { throw new RuntimeException('License not found.'); } clm_update_license_record($license, [ 'owner' => $_POST['owner'] ?? '', 'customer_name' => $_POST['customer_name'] ?? '', 'customer_email' => $_POST['customer_email'] ?? '', 'app_slug' => $_POST['app_slug'] ?? '', 'max_activations' => $_POST['max_activations'] ?? 1, 'max_counters' => $_POST['max_counters'] ?? 1, 'expires_at' => $_POST['expires_at'] ?? '', 'status' => $_POST['status'] ?? 'active', 'notes' => $_POST['notes'] ?? '', ]); clm_set_flash('success', 'License updated.'); break; case 'toggle_license': $license = clm_license_with_app_by_id((int)($_POST['id'] ?? 0)); if (!$license) { throw new RuntimeException('License not found.'); } $nextStatus = ($license['status'] ?? 'active') === 'active' ? 'suspended' : 'active'; clm_update_license_record($license, ['status' => $nextStatus]); clm_set_flash('success', 'License status changed to ' . $nextStatus . '.'); break; case 'deactivate_activation': $id = (int)($_POST['id'] ?? 0); if ($id <= 0) { throw new RuntimeException('Activation not found.'); } $stmt = clm_db()->prepare("UPDATE `{$activationsTable}` SET status = 'deactivated', deactivated_at = NOW(), last_seen_at = NOW() WHERE id = ?"); $stmt->execute([$id]); clm_set_flash('success', 'Activation deactivated.'); break; } } catch (Throwable $e) { clm_set_flash('error', $e->getMessage()); } clm_redirect('manage.php'); } $flash = clm_get_flash(); if (!$isAuthenticated): ?> Central License Manager Login
Admin access

Central License Manager

Use the configured admin password to manage apps, licenses, and activations from one place.

Mode:
Back to module home
0, 'licenses' => 0, 'activations' => 0, ]; $apps = []; $licenses = []; $activations = []; try { $pdo = clm_db(); $stats['apps'] = (int)$pdo->query("SELECT COUNT(*) FROM `{$appsTable}`")->fetchColumn(); $stats['licenses'] = (int)$pdo->query("SELECT COUNT(*) FROM `{$licensesTable}`")->fetchColumn(); $stats['activations'] = (int)$pdo->query("SELECT COUNT(*) FROM `{$activationsTable}` WHERE status = 'active'")->fetchColumn(); $apps = $pdo->query("SELECT a.*, (SELECT COUNT(*) FROM `{$licensesTable}` l WHERE l.app_id = a.id) AS licenses_count, (SELECT COUNT(*) FROM `{$activationsTable}` act INNER JOIN `{$licensesTable}` l2 ON l2.id = act.license_id WHERE l2.app_id = a.id AND act.status = 'active') AS active_activations FROM `{$appsTable}` a ORDER BY a.name ASC")->fetchAll(); $licenses = $pdo->query("SELECT l.*, a.slug AS app_slug, a.name AS app_name, (SELECT COUNT(*) FROM `{$activationsTable}` act WHERE act.license_id = l.id AND act.status = 'active') AS active_activations FROM `{$licensesTable}` l INNER JOIN `{$appsTable}` a ON a.id = l.app_id ORDER BY l.created_at DESC LIMIT 200")->fetchAll(); $activations = $pdo->query("SELECT act.*, l.license_key, a.slug AS app_slug, a.name AS app_name FROM `{$activationsTable}` act INNER JOIN `{$licensesTable}` l ON l.id = act.license_id INNER JOIN `{$appsTable}` a ON a.id = l.app_id ORDER BY COALESCE(act.last_seen_at, act.activated_at) DESC LIMIT 200")->fetchAll(); } catch (Throwable $e) { $flash = ['type' => 'error', 'message' => $e->getMessage()]; } $baseUrl = clm_base_url(); ?> Central License Manager Dashboard
Multi-app dashboard

Manage licenses and installations for multiple products from one isolated module. This dashboard is intentionally separate from the old in-app license manager so you can move it later.

Apps
Licenses
Active installs
Manager date

Create app

You can rename the app or adjust its slug later in the Apps table.

Issue license

What to point clients to

Each app should call this module, not keep its own separate activation database.

Current API stays backward compatible with older clients by returning both max_activations and allowed_activations.

Apps

Name Slug Status Licenses Active installs Action
No apps yet.
Update client settings too if you rename this slug.

Licenses

No licenses yet.

·
Created:
Active installs:

Recent activations

License App Domain Fingerprint Product Status Activated Last seen Action
No activations yet.
Inactive