476 lines
18 KiB
JavaScript
476 lines
18 KiB
JavaScript
/**
|
|
* SmartIMS License Management CLI
|
|
*
|
|
* Usage: node tools/license_manager.cjs <command> [args]
|
|
*
|
|
* Commands:
|
|
* list : Show all system modules and their status from DB
|
|
* generate <module> <type> : Generate a new signed license key (Offline)
|
|
* decode <key> : Decode and inspect a license key (Offline)
|
|
* activate <key> : Activate a module using a key (Updates DB)
|
|
* delete <module> : Deactivate/Remove a module (Updates DB)
|
|
*/
|
|
|
|
const path = require('path');
|
|
require('dotenv').config({ path: path.resolve(__dirname, '../server/.env') });
|
|
const { generateLicense, verifyLicense } = require('../server/utils/licenseManager');
|
|
|
|
// Try to load mysql2 from server dependencies
|
|
let mysql;
|
|
try {
|
|
mysql = require(path.resolve(__dirname, '../server/node_modules/mysql2/promise'));
|
|
} catch (e) {
|
|
try {
|
|
mysql = require('mysql2/promise');
|
|
} catch (e2) {
|
|
console.error('Error: mysql2 module not found. Please install dependencies in server directory.');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// DB Connection Config
|
|
const dbConfig = {
|
|
host: process.env.DB_HOST || 'localhost',
|
|
user: process.env.DB_USER || 'root',
|
|
password: process.env.DB_PASSWORD,
|
|
database: process.env.DB_NAME || 'sokuree_platform_dev',
|
|
port: process.env.DB_PORT || 3306
|
|
};
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
// Basic argument parsing
|
|
function parseArgs(args) {
|
|
const params = { positional: [] };
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i].startsWith('--')) {
|
|
const key = args[i].substring(2);
|
|
// Check if next is value or flag
|
|
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
params[key] = args[i + 1];
|
|
i++;
|
|
} else {
|
|
params[key] = true;
|
|
}
|
|
} else {
|
|
params.positional.push(args[i]);
|
|
}
|
|
}
|
|
return params;
|
|
}
|
|
|
|
const params = parseArgs(args);
|
|
const command = params.positional[0];
|
|
|
|
if (!command) {
|
|
printUsage();
|
|
process.exit(1);
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
switch (command) {
|
|
case 'list':
|
|
await listModules();
|
|
break;
|
|
case 'generate':
|
|
// Pass positional args: command is [0], module is [1], type is [2]
|
|
await generateCmd(params.positional[1], params.positional[2]);
|
|
break;
|
|
case 'decode':
|
|
await decodeCmd(params.positional[1]);
|
|
break;
|
|
case 'activate':
|
|
await activateCmd(params.positional[1]);
|
|
break;
|
|
case 'delete':
|
|
await deleteCmd(params.positional[1]);
|
|
break;
|
|
case 'show':
|
|
await showCmd(params.positional[1]);
|
|
break;
|
|
case 'help':
|
|
printUsage();
|
|
break;
|
|
default:
|
|
console.error(`Unknown command: ${command}`);
|
|
printUsage();
|
|
process.exit(1);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error:', err.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function printUsage() {
|
|
console.log(`
|
|
SmartIMS License Manager
|
|
Usage: node tools/license_manager.cjs <command> [args]
|
|
|
|
Commands:
|
|
list Show active modules in Database
|
|
generate <module> <type> Generate a new license key
|
|
Modules: asset, production, monitoring
|
|
Types: dev, sub, demo
|
|
Flags: --subscriber <id> (REQUIRED)
|
|
[--activate]
|
|
[--activation-window <days>]
|
|
[--expiry <YYYY-MM-DD>]
|
|
decode <key> Decrypt and show key details
|
|
activate <key> Activate module in DB using key
|
|
delete <module> Deactivate module in DB
|
|
`);
|
|
}
|
|
|
|
// --- Commands ---
|
|
|
|
async function listModules() {
|
|
const conn = await mysql.createConnection(dbConfig);
|
|
try {
|
|
console.log('\n=== Active System Modules ===');
|
|
const [rows] = await conn.execute('SELECT * FROM system_modules');
|
|
console.log('---------------------------------------------------------------------------------------');
|
|
console.log('| Code | Active | Type | Expiry | Subscriber |');
|
|
console.log('---------------------------------------------------------------------------------------');
|
|
|
|
const defaults = ['asset', 'production', 'monitoring'];
|
|
const data = {};
|
|
rows.forEach(r => data[r.code] = r);
|
|
|
|
defaults.forEach(code => {
|
|
const mod = data[code];
|
|
const active = mod && mod.is_active ? 'YES' : 'NO ';
|
|
const type = mod ? (mod.license_type || 'N/A') : 'N/A';
|
|
const expiry = mod && mod.expiry_date ? new Date(mod.expiry_date).toLocaleDateString() : 'Permanent/None';
|
|
const sub = mod && mod.subscriber_id ? mod.subscriber_id : 'N/A';
|
|
console.log(`| ${code.padEnd(12)} | ${active.padEnd(6)} | ${type.padEnd(4)} | ${expiry.padEnd(20)} | ${sub.padEnd(18)} |`);
|
|
});
|
|
console.log('---------------------------------------------------------------------------------------\n');
|
|
|
|
console.log('=== Recent Issued Keys (Last 10) ===');
|
|
const [issues] = await conn.execute('SELECT * FROM issued_licenses ORDER BY id DESC LIMIT 10');
|
|
if (issues.length === 0) {
|
|
console.log('(No issued keys found)');
|
|
} else {
|
|
console.log('-------------------------------------------------------------------------------------------------------');
|
|
console.log('| ID | Date | Module | Type | Subscriber | Status | Deadline |');
|
|
console.log('-------------------------------------------------------------------------------------------------------');
|
|
issues.forEach(r => {
|
|
const date = new Date(r.created_at).toLocaleDateString();
|
|
const deadline = r.activation_deadline ? new Date(r.activation_deadline).toLocaleDateString() : 'None';
|
|
// Check if status needs update based on deadline? (Display logic only)
|
|
let status = r.status || 'ISSUED';
|
|
if (status === 'ISSUED' && r.activation_deadline && new Date() > new Date(r.activation_deadline)) {
|
|
status = 'EXPIRED';
|
|
}
|
|
|
|
console.log(`| ${r.id.toString().padEnd(4)} | ${date.padEnd(10)} | ${r.module_code.padEnd(12)} | ${r.license_type.padEnd(4)} | ${(r.subscriber_id || 'N/A').padEnd(10)} | ${status.padEnd(10)} | ${deadline.padEnd(20)} |`);
|
|
});
|
|
console.log('-------------------------------------------------------------------------------------------------------\n');
|
|
console.log('Tip: Use "show <id>" to view the full license key.\n');
|
|
}
|
|
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
}
|
|
|
|
async function showCmd(identifier) {
|
|
if (!identifier) {
|
|
console.error('Usage: show <id> OR show <module_code>');
|
|
return;
|
|
}
|
|
|
|
const conn = await mysql.createConnection(dbConfig);
|
|
try {
|
|
// Check if identifier is a known module code
|
|
const validModules = ['asset', 'production', 'monitoring'];
|
|
const isModule = validModules.includes(identifier) || validModules.includes(identifier.toLowerCase());
|
|
|
|
if (isModule) {
|
|
// Show Active Module License
|
|
const code = identifier.toLowerCase() === 'cctv' ? 'monitoring' : identifier.toLowerCase();
|
|
const [rows] = await conn.execute('SELECT * FROM system_modules WHERE code = ?', [code]);
|
|
|
|
if (rows.length === 0 || !rows[0].is_active) {
|
|
console.log(`\nModule '${code}' is NOT active or has no license.\n`);
|
|
return;
|
|
}
|
|
|
|
const r = rows[0];
|
|
console.log(`\n=== Active License: ${r.name} (${r.code}) ===`);
|
|
console.log(`Subscriber : ${r.subscriber_id || 'N/A'}`);
|
|
console.log(`Type : ${r.license_type}`);
|
|
console.log(`Expiry : ${r.expiry_date ? new Date(r.expiry_date).toLocaleDateString() : 'Permanent'}`);
|
|
console.log('--------------------------------------------------');
|
|
console.log('KEY:');
|
|
console.log(r.license_key);
|
|
console.log('--------------------------------------------------\n');
|
|
|
|
} else {
|
|
// Assume ID for Issued License
|
|
const id = parseInt(identifier, 10);
|
|
if (isNaN(id)) {
|
|
console.error(`Error: '${identifier}' is not a valid ID or Module Code.`);
|
|
return;
|
|
}
|
|
|
|
const [rows] = await conn.execute('SELECT * FROM issued_licenses WHERE id = ?', [id]);
|
|
if (rows.length === 0) {
|
|
console.error(`Error: License with ID ${id} not found.`);
|
|
return;
|
|
}
|
|
|
|
const r = rows[0];
|
|
console.log('\n=== Issued License Details (ID: ' + r.id + ') ===');
|
|
console.log(`Module : ${r.module_code}`);
|
|
console.log(`Type : ${r.license_type}`);
|
|
console.log(`Subscriber : ${r.subscriber_id || 'N/A'}`);
|
|
console.log(`Status : ${r.status}`);
|
|
console.log(`Created : ${new Date(r.created_at).toLocaleString()}`);
|
|
console.log(`Deadline : ${r.activation_deadline ? new Date(r.activation_deadline).toLocaleString() : 'None'}`);
|
|
console.log('--------------------------------------------------');
|
|
console.log('KEY:');
|
|
console.log(r.license_key);
|
|
console.log('--------------------------------------------------\n');
|
|
}
|
|
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
}
|
|
|
|
async function generateCmd(moduleCode, type) {
|
|
const subscriberId = params.subscriber;
|
|
const shouldActivate = params.activate;
|
|
const activationWindow = params['activation-window'];
|
|
const expiryDate = params['expiry']; // e.g. "2026-12-31"
|
|
|
|
if (!moduleCode || !type || !subscriberId) {
|
|
console.log('Usage: generate <module> <type> --subscriber <id> [--activate] [--activation-window <days>] [--expiry <YYYY-MM-DD>]');
|
|
return;
|
|
}
|
|
|
|
// ... Validation (Same as before) ...
|
|
const validModules = ['asset', 'production', 'monitoring'];
|
|
const validTypes = ['dev', 'sub', 'demo'];
|
|
const modMap = { 'cctv': 'monitoring' };
|
|
const finalCode = modMap[moduleCode] || moduleCode;
|
|
|
|
if (!validModules.includes(finalCode)) {
|
|
console.error(`Invalid module. Allowed: ${validModules.join(', ')}`);
|
|
return;
|
|
}
|
|
if (!validTypes.includes(type)) {
|
|
console.error(`Invalid type. Allowed: ${validTypes.join(', ')}`);
|
|
return;
|
|
}
|
|
|
|
if (expiryDate && isNaN(Date.parse(expiryDate))) {
|
|
console.error('Invalid expiry date format. Use YYYY-MM-DD.');
|
|
return;
|
|
}
|
|
|
|
const days = activationWindow ? parseInt(activationWindow, 10) : null;
|
|
|
|
// Load Private Key
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const privateKeyPath = path.join(__dirname, 'keys', 'private_key.pem');
|
|
|
|
let privateKey;
|
|
try {
|
|
privateKey = fs.readFileSync(privateKeyPath, 'utf8');
|
|
} catch (e) {
|
|
console.error('Error: Could not load private_key.pem from tools/keys/. Run generate_keys.cjs first.');
|
|
return;
|
|
}
|
|
|
|
const key = generateLicense(finalCode, type, subscriberId, days, expiryDate, privateKey);
|
|
|
|
// Save to issued_licenses
|
|
const conn = await mysql.createConnection(dbConfig);
|
|
try {
|
|
const deadline = days ? new Date(Date.now() + (days * 24 * 60 * 60 * 1000)) : null;
|
|
await conn.execute(
|
|
`INSERT INTO issued_licenses (module_code, license_key, license_type, subscriber_id, status, activation_deadline)
|
|
VALUES (?, ?, ?, ?, 'ISSUED', ?)`,
|
|
[finalCode, key, type, subscriberId, deadline]
|
|
);
|
|
} catch (e) {
|
|
console.error('Warning: Failed to log issued license to DB:', e.message);
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
|
|
console.log('\nGENERATED LICENSE KEY:');
|
|
if (days) {
|
|
console.log(`(NOTE: This key MUST be activated within ${days} days)`);
|
|
}
|
|
if (expiryDate) {
|
|
console.log(`(NOTE: This key will expire on ${expiryDate})`);
|
|
}
|
|
console.log('--------------------------------------------------');
|
|
console.log(key);
|
|
console.log('--------------------------------------------------\n');
|
|
|
|
if (shouldActivate) {
|
|
console.log('Auto-activating...');
|
|
await activateCmd(key);
|
|
}
|
|
}
|
|
|
|
async function decodeCmd(key) {
|
|
if (!key) {
|
|
console.error('Usage: decode <key_string>');
|
|
return;
|
|
}
|
|
|
|
// Load Public Key
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const publicKeyPath = path.join(__dirname, 'keys', 'public_key.pem');
|
|
|
|
let publicKey;
|
|
try {
|
|
publicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
|
} catch (e) {
|
|
console.error('Error: Could not load public_key.pem from tools/keys/. Run generate_keys.cjs first.');
|
|
return;
|
|
}
|
|
|
|
const result = verifyLicense(key, publicKey);
|
|
console.log('\nLICENSE DECODE RESULT:');
|
|
console.log('--------------------------------------------------');
|
|
console.log(`Valid Signature : ${result.isValid ? 'YES' : 'NO'}`);
|
|
if (!result.isValid) {
|
|
console.log(`Reason : ${result.reason}`);
|
|
}
|
|
|
|
if (result.module) {
|
|
// Even if invalid (expired), we might want to show details if signature was ok?
|
|
// verifyLicense returns payload details if valid signature but expired?
|
|
// Let's check verifyLicense implementation.
|
|
// It returns { isValid: false, reason: 'Expired', ... } if expired.
|
|
// So we can show details.
|
|
|
|
console.log(`Module : ${result.module}`);
|
|
console.log(`Type : ${result.type}`);
|
|
console.log(`Subscriber : ${result.subscriberId}`);
|
|
console.log(`Expiry Date : ${result.expiryDate ? new Date(result.expiryDate).toLocaleDateString() : 'Permanent'}`);
|
|
}
|
|
console.log('--------------------------------------------------\n');
|
|
}
|
|
|
|
async function activateCmd(key) {
|
|
if (!key) {
|
|
console.error('Usage: activate <key_string>');
|
|
return;
|
|
}
|
|
|
|
// Load Public Key for Verification
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const publicKeyPath = path.join(__dirname, 'keys', 'public_key.pem');
|
|
|
|
let publicKey;
|
|
try {
|
|
publicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
|
} catch (e) {
|
|
console.error('Error: Could not load public_key.pem from tools/keys/. Run generate_keys.cjs first.');
|
|
return;
|
|
}
|
|
|
|
const result = verifyLicense(key, publicKey);
|
|
if (!result.isValid) {
|
|
console.error(`Invalid License: ${result.reason}`);
|
|
return;
|
|
}
|
|
|
|
const conn = await mysql.createConnection(dbConfig);
|
|
try {
|
|
// ... Subscriber Check (Same) ...
|
|
const [settings] = await conn.execute("SELECT setting_value FROM system_settings WHERE setting_key = 'subscriber_id'");
|
|
const serverSubscriberId = settings.length > 0 ? settings[0].setting_value : null;
|
|
|
|
if (!serverSubscriberId) {
|
|
console.error('\nERROR: Server Subscriber ID is not set in DB. Please configure it first.');
|
|
return;
|
|
}
|
|
|
|
if (result.subscriberId !== serverSubscriberId) {
|
|
console.error(`\nERROR: License Subscriber Mismatch.`);
|
|
console.error(`License is for: '${result.subscriberId}'`);
|
|
console.error(`Server is set to: '${serverSubscriberId}'`);
|
|
return;
|
|
}
|
|
|
|
// 3. Archive Current License
|
|
const [current] = await conn.execute('SELECT * FROM system_modules WHERE code = ?', [result.module]);
|
|
if (current.length > 0 && current[0].is_active && current[0].license_key) {
|
|
const old = current[0];
|
|
const historySql = `
|
|
INSERT INTO license_history (module_code, license_key, license_type, subscriber_id, activated_at)
|
|
VALUES (?, ?, ?, ?, NOW())
|
|
`;
|
|
await conn.execute(historySql, [old.code, old.license_key, old.license_type, old.subscriber_id]);
|
|
|
|
// Also mark old key as replaced/archived in issued_licenses if exists?
|
|
// Optional, but good for consistency.
|
|
await conn.execute("UPDATE issued_licenses SET status = 'REPLACED' WHERE license_key = ?", [old.license_key]);
|
|
}
|
|
|
|
// 4. Activate New License
|
|
const names = { 'asset': '자산 관리', 'production': '생산 관리', 'monitoring': 'CCTV' };
|
|
const sql = `
|
|
INSERT INTO system_modules (code, name, is_active, license_key, license_type, expiry_date, subscriber_id)
|
|
VALUES (?, ?, true, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
is_active = true,
|
|
license_key = VALUES(license_key),
|
|
license_type = VALUES(license_type),
|
|
expiry_date = VALUES(expiry_date),
|
|
subscriber_id = VALUES(subscriber_id)
|
|
`;
|
|
|
|
await conn.execute(sql, [result.module, names[result.module] || result.module, key, result.type, result.expiryDate, result.subscriberId]);
|
|
|
|
// 5. Update Issued License Status
|
|
await conn.execute("UPDATE issued_licenses SET status = 'ACTIVE' WHERE license_key = ?", [key]);
|
|
|
|
console.log(`\nSUCCESS: Module '${result.module}' activated.`);
|
|
console.log(`Type: ${result.type}, Subscriber: ${result.subscriberId}, Expiry: ${result.expiryDate || 'Permanent'}\n`);
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
}
|
|
|
|
async function deleteCmd(moduleCode) {
|
|
if (!moduleCode) {
|
|
console.error('Usage: delete <module_code>');
|
|
return;
|
|
}
|
|
|
|
const validModules = ['asset', 'production', 'monitoring'];
|
|
if (!validModules.includes(moduleCode)) {
|
|
console.error(`Invalid module. Allowed: ${validModules.join(', ')}`);
|
|
return;
|
|
}
|
|
|
|
const conn = await mysql.createConnection(dbConfig);
|
|
try {
|
|
// We set is_active = false
|
|
const [res] = await conn.execute('UPDATE system_modules SET is_active = false WHERE code = ?', [moduleCode]);
|
|
if (res.affectedRows > 0) {
|
|
console.log(`\nSUCCESS: Module '${moduleCode}' deactivated.\n`);
|
|
} else {
|
|
console.log(`\nModule '${moduleCode}' not found or already inactive.\n`);
|
|
}
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
}
|
|
|
|
main();
|