smart_ims/tools/license_manager.cjs
2026-01-23 16:52:14 +09:00

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