/** * SmartAsset License Management CLI * * Usage: node tools/license_manager.cjs [args] * * Commands: * list : Show all system modules and their status from DB * generate : Generate a new signed license key (Offline) * decode : Decode and inspect a license key (Offline) * activate : Activate a module using a key (Updates DB) * delete : 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 || 'smartasset_db', 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(` SmartAsset License Manager Usage: node tools/license_manager.cjs [args] Commands: list Show active modules in Database generate Generate a new license key Modules: asset, production, monitoring Types: dev, sub, demo Flags: --subscriber (REQUIRED) [--activate] [--activation-window ] [--expiry ] decode Decrypt and show key details activate Activate module in DB using key delete 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 " to view the full license key.\n'); } } finally { await conn.end(); } } async function showCmd(identifier) { if (!identifier) { console.error('Usage: show OR show '); 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 --subscriber [--activate] [--activation-window ] [--expiry ]'); 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 '); 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 '); 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 '); 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();