615 lines
25 KiB
JavaScript
615 lines
25 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const { exec, execSync } = require('child_process');
|
|
const db = require('../db');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { isAuthenticated, hasRole } = require('../middleware/authMiddleware');
|
|
const { generateLicense, verifyLicense } = require('../utils/licenseManager');
|
|
const { checkRemoteKey } = require('../utils/remoteLicense');
|
|
const cryptoUtil = require('../utils/cryptoUtil');
|
|
|
|
// Load Public Key for Verification
|
|
const publicKeyPath = path.join(__dirname, '../config/public_key.pem');
|
|
let publicKey = null;
|
|
try {
|
|
if (fs.existsSync(publicKeyPath)) {
|
|
publicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
|
console.log('✅ Public Key loaded successfully for license verification');
|
|
} else {
|
|
console.error('❌ WARNING: public_key.pem not found at:', publicKeyPath);
|
|
}
|
|
} catch (e) {
|
|
console.error('❌ Error loading public key:', e);
|
|
}
|
|
|
|
// Helper to check if a setting key is allowed for general get/post
|
|
// This prevents modification of sensitive keys if any
|
|
const ALLOWED_SETTING_KEYS = [
|
|
'subscriber_id',
|
|
'session_timeout',
|
|
'encryption_key',
|
|
'asset_id_rule',
|
|
'asset_categories',
|
|
'asset_locations',
|
|
'asset_statuses',
|
|
'asset_maintenance_types',
|
|
'gitea_url',
|
|
'gitea_user',
|
|
'gitea_password'
|
|
];
|
|
|
|
// --- .env File Utilities ---
|
|
const envPath = path.join(__dirname, '../.env');
|
|
|
|
const readEnv = () => {
|
|
if (!fs.existsSync(envPath)) return {};
|
|
const content = fs.readFileSync(envPath, 'utf8');
|
|
const lines = content.split('\n');
|
|
const env = {};
|
|
lines.forEach(line => {
|
|
const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
|
|
if (match) {
|
|
env[match[1]] = match[2] ? match[2].trim() : '';
|
|
}
|
|
});
|
|
return env;
|
|
};
|
|
|
|
const writeEnv = (updates) => {
|
|
let content = fs.readFileSync(envPath, 'utf8');
|
|
Object.entries(updates).forEach(([key, value]) => {
|
|
const regex = new RegExp(`^${key}=.*`, 'm');
|
|
if (regex.test(content)) {
|
|
content = content.replace(regex, `${key}=${value}`);
|
|
} else {
|
|
content += `\n${key}=${value}`;
|
|
}
|
|
});
|
|
fs.writeFileSync(envPath, content, 'utf8');
|
|
};
|
|
|
|
const mysql = require('mysql2/promise');
|
|
|
|
// 0. Server Configuration (Subscriber ID & Session Timeout)
|
|
router.get('/settings', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
try {
|
|
const [rows] = await db.query("SELECT setting_key, setting_value FROM system_settings WHERE setting_key IN ('subscriber_id', 'session_timeout', 'encryption_key', 'gitea_url', 'gitea_user', 'gitea_password')");
|
|
const settings = {};
|
|
rows.forEach(r => settings[r.setting_key] = r.setting_value);
|
|
|
|
// Include .env DB settings
|
|
const env = readEnv();
|
|
|
|
res.json({
|
|
subscriber_id: settings.subscriber_id || '',
|
|
session_timeout: parseInt(settings.session_timeout) || 60,
|
|
encryption_key: settings.encryption_key || '',
|
|
gitea_url: settings.gitea_url || 'https://gitea.qideun.com/SOKUREE/smart_ims.git',
|
|
gitea_user: settings.gitea_user || '',
|
|
gitea_password: settings.gitea_password ? cryptoUtil.decryptMasterKey(settings.gitea_password) : '',
|
|
db_config: {
|
|
host: env.DB_HOST || '',
|
|
user: env.DB_USER || '',
|
|
password: env.DB_PASSWORD || '',
|
|
database: env.DB_NAME || '',
|
|
port: env.DB_PORT || '3306'
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Database error' });
|
|
}
|
|
});
|
|
|
|
router.post('/settings', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
const { subscriber_id, session_timeout, encryption_key, db_config } = req.body;
|
|
|
|
try {
|
|
if (subscriber_id !== undefined) {
|
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('subscriber_id', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [subscriber_id]);
|
|
}
|
|
if (session_timeout !== undefined) {
|
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('session_timeout', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [session_timeout.toString()]);
|
|
}
|
|
if (encryption_key !== undefined) {
|
|
const encryptedKeyForDb = cryptoUtil.encryptMasterKey(encryption_key);
|
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('encryption_key', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [encryptedKeyForDb]);
|
|
}
|
|
if (req.body.gitea_url !== undefined) {
|
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('gitea_url', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [req.body.gitea_url]);
|
|
}
|
|
if (req.body.gitea_user !== undefined) {
|
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('gitea_user', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [req.body.gitea_user]);
|
|
}
|
|
if (req.body.gitea_password !== undefined) {
|
|
const encryptedPass = cryptoUtil.encryptMasterKey(req.body.gitea_password);
|
|
await db.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('gitea_password', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [encryptedPass]);
|
|
}
|
|
|
|
// Handle .env DB settings
|
|
if (db_config) {
|
|
writeEnv({
|
|
DB_HOST: db_config.host,
|
|
DB_USER: db_config.user,
|
|
DB_PASSWORD: db_config.password,
|
|
DB_NAME: db_config.database,
|
|
DB_PORT: db_config.port
|
|
});
|
|
}
|
|
|
|
res.json({ message: 'Settings saved. Server may restart to apply DB changes.' });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Database error' });
|
|
}
|
|
});
|
|
|
|
// --- Crypto & Key Rotation ---
|
|
|
|
// 0.2 Test DB Connection
|
|
router.post('/test-db', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
const { host, user, password, database, port } = req.body;
|
|
|
|
let conn;
|
|
try {
|
|
conn = await mysql.createConnection({
|
|
host,
|
|
user,
|
|
password,
|
|
database,
|
|
port: parseInt(port) || 3306,
|
|
connectTimeout: 5000
|
|
});
|
|
await conn.query('SELECT 1');
|
|
res.json({ success: true, message: '연결 성공: 데이터베이스에 성공적으로 접속되었습니다.' });
|
|
} catch (err) {
|
|
res.status(400).json({ success: false, error: err.message });
|
|
} finally {
|
|
if (conn) await conn.end();
|
|
}
|
|
});
|
|
|
|
// 0.3 Encryption Key Management & Rotation
|
|
router.get('/encryption/status', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
try {
|
|
const [userRows] = await db.query('SELECT COUNT(*) as count FROM users WHERE phone IS NOT NULL AND phone LIKE "%:%"');
|
|
const currentKey = await cryptoUtil.getMasterKey();
|
|
|
|
res.json({
|
|
current_key: currentKey,
|
|
affected_records: {
|
|
users: userRows[0].count
|
|
}
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Failed to fetch encryption status' });
|
|
}
|
|
});
|
|
|
|
router.post('/encryption/rotate', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
const { new_key } = req.body;
|
|
if (!new_key) return res.status(400).json({ error: 'New key is required' });
|
|
|
|
const conn = await db.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
|
|
const oldKey = await cryptoUtil.getMasterKey();
|
|
|
|
// 1. Migrate Users Table (phone)
|
|
const [users] = await conn.query('SELECT id, phone FROM users WHERE phone IS NOT NULL AND phone LIKE "%:%"');
|
|
for (const user of users) {
|
|
const decrypted = await cryptoUtil.decrypt(user.phone, oldKey);
|
|
const reEncrypted = await cryptoUtil.encrypt(decrypted, new_key);
|
|
await conn.query('UPDATE users SET phone = ? WHERE id = ?', [reEncrypted, user.id]);
|
|
}
|
|
|
|
// 2. Update Master Key in settings (Encrypted for DB storage)
|
|
const encryptedKeyForDb = cryptoUtil.encryptMasterKey(new_key);
|
|
await conn.query(`INSERT INTO system_settings (setting_key, setting_value) VALUES ('encryption_key', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`, [encryptedKeyForDb]);
|
|
|
|
await conn.commit();
|
|
cryptoUtil.clearCache(); // Force immediate reload of new key
|
|
|
|
res.json({ success: true, message: 'Encryption key rotated and data migrated successfully.' });
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
console.error('Rotation failed:', err);
|
|
res.status(500).json({ error: 'Key rotation failed: ' + err.message });
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
});
|
|
|
|
// 0-1. Generic Setting Get/Set
|
|
router.get('/settings/:key', isAuthenticated, async (req, res) => {
|
|
const { key } = req.params;
|
|
if (!ALLOWED_SETTING_KEYS.includes(key)) {
|
|
return res.status(400).json({ error: 'Invalid setting key' });
|
|
}
|
|
|
|
try {
|
|
const [rows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = ?", [key]);
|
|
if (rows.length === 0) return res.json({ value: null });
|
|
res.json({ value: rows[0].setting_value });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Database error' });
|
|
}
|
|
});
|
|
|
|
router.post('/settings/:key', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
const { key } = req.params;
|
|
const { value } = req.body;
|
|
|
|
if (!ALLOWED_SETTING_KEYS.includes(key)) {
|
|
return res.status(400).json({ error: 'Invalid setting key' });
|
|
}
|
|
|
|
try {
|
|
let stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
|
|
// Special handling for sensitive keys to protect it in DB
|
|
if (key === 'encryption_key' || key === 'gitea_password') {
|
|
stringValue = cryptoUtil.encryptMasterKey(stringValue);
|
|
}
|
|
|
|
await db.query(
|
|
`INSERT INTO system_settings (setting_key, setting_value)
|
|
VALUES (?, ?)
|
|
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`,
|
|
[key, stringValue]
|
|
);
|
|
res.json({ success: true, message: 'Setting saved' });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Database error' });
|
|
}
|
|
});
|
|
|
|
// 1. Get All Modules Status
|
|
router.get('/modules', isAuthenticated, async (req, res) => {
|
|
try {
|
|
const [rows] = await db.query('SELECT * FROM system_modules');
|
|
|
|
const modules = {};
|
|
const defaults = ['asset', 'production', 'monitoring'];
|
|
|
|
// Get stored subscriber ID
|
|
const [subRows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'subscriber_id'");
|
|
const serverSubscriberId = subRows.length > 0 ? subRows[0].setting_value : null;
|
|
|
|
defaults.forEach(code => {
|
|
const found = rows.find(r => r.code === code);
|
|
if (found) {
|
|
modules[code] = {
|
|
active: !!found.is_active,
|
|
type: found.license_type,
|
|
expiry: found.expiry_date,
|
|
subscriber_id: found.subscriber_id // Return who verified it
|
|
};
|
|
} else {
|
|
modules[code] = { active: false, type: null, expiry: null, subscriber_id: null };
|
|
}
|
|
});
|
|
|
|
res.json({ modules, serverSubscriberId });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Database error' });
|
|
}
|
|
});
|
|
|
|
const axios = require('axios');
|
|
|
|
// 2. Activate Module (Apply License)
|
|
// Only admin can manage system modules
|
|
router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
const { code } = req.params;
|
|
let { licenseKey } = req.body;
|
|
|
|
if (!licenseKey) {
|
|
return res.status(400).json({ error: 'License key is required' });
|
|
}
|
|
|
|
licenseKey = licenseKey.trim();
|
|
|
|
// 1. Verify Key validity
|
|
const result = verifyLicense(licenseKey, publicKey);
|
|
if (!result.isValid) {
|
|
return res.status(400).json({ error: `Invalid License: ${result.reason}` });
|
|
}
|
|
|
|
// 2. Check Module match
|
|
if (result.module !== code) {
|
|
return res.status(400).json({ error: `This license is for '${result.module}' module, not '${code}'` });
|
|
}
|
|
|
|
// 3. Check Subscriber match
|
|
try {
|
|
const [subRows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'subscriber_id'");
|
|
const serverSubscriberId = subRows.length > 0 ? subRows[0].setting_value : null;
|
|
|
|
if (!serverSubscriberId) {
|
|
return res.status(400).json({ error: '서버 구독자 ID가 설정되지 않았습니다. [서버 환경 설정]에서 먼저 설정해주세요.' });
|
|
}
|
|
|
|
// Allow 'dev' type to bypass strict subscriber check, but log it
|
|
if (result.type === 'dev') {
|
|
if (result.subscriberId !== serverSubscriberId) {
|
|
console.warn(`⚠️ Dev License used: Subscriber ID mismatch allowed. Key: ${result.subscriberId}, Server: ${serverSubscriberId}`);
|
|
}
|
|
} else {
|
|
if (result.subscriberId !== serverSubscriberId) {
|
|
return res.status(403).json({
|
|
error: `구독자 ID 불일치: 라이선스 키는 [${result.subscriberId}] 전용이지만, 현재 서버는 [${serverSubscriberId}]로 설정되어 있습니다.`
|
|
});
|
|
}
|
|
}
|
|
|
|
// 4. Archive Current License if exists
|
|
const [current] = await db.query('SELECT * FROM system_modules WHERE code = ?', [code]);
|
|
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 db.query(historySql, [old.code, old.license_key, old.license_type, old.subscriber_id]);
|
|
}
|
|
|
|
// Upsert into system_modules
|
|
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)
|
|
`;
|
|
|
|
// Map codes to names
|
|
const names = {
|
|
'asset': '자산 관리',
|
|
'production': '생산 관리',
|
|
'monitoring': 'CCTV'
|
|
};
|
|
|
|
await db.query(sql, [code, names[code] || code, licenseKey, result.type, result.expiryDate, result.subscriberId]);
|
|
|
|
// 5. Sync status with License Manager
|
|
const licenseManagerUrl = process.env.LICENSE_MANAGER_URL || 'http://localhost:3006/api';
|
|
try {
|
|
await axios.post(`${licenseManagerUrl}/licenses/activate`, { licenseKey });
|
|
console.log(`✅ Synced activation status for key: ${licenseKey.substring(0, 20)}...`);
|
|
} catch (syncErr) {
|
|
const errorDetail = syncErr.response ?
|
|
`Status: ${syncErr.response.status}, Data: ${JSON.stringify(syncErr.response.data)}` :
|
|
syncErr.message;
|
|
console.error(`⚠️ Failed to sync status with License Manager: ${errorDetail}`);
|
|
// We don't fail the whole activation if sync fails, but we log it
|
|
}
|
|
|
|
res.json({ success: true, message: 'Module activated', type: result.type, expiry: result.expiryDate });
|
|
|
|
} catch (err) {
|
|
console.error('❌ License Activation Error:', err);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: err.message || 'Database error',
|
|
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
|
|
});
|
|
}
|
|
});
|
|
|
|
// 3. Get Module History
|
|
router.get('/modules/:code/history', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
try {
|
|
const [rows] = await db.query('SELECT * FROM license_history WHERE module_code = ? ORDER BY id DESC LIMIT 10', [req.params.code]);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Database error' });
|
|
}
|
|
});
|
|
|
|
// 4. Deactivate Module
|
|
router.post('/modules/:code/deactivate', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
const { code } = req.params;
|
|
try {
|
|
// Archive on deactivate too?
|
|
const [current] = await db.query('SELECT * FROM system_modules WHERE code = ?', [code]);
|
|
if (current.length > 0 && current[0].is_active) {
|
|
const old = current[0];
|
|
const historySql = `
|
|
INSERT INTO license_history (module_code, license_key, license_type, subscriber_id, activated_at)
|
|
VALUES (?, ?, ?, ?, NOW())
|
|
`;
|
|
await db.query(historySql, [old.code, old.license_key, old.license_type, old.subscriber_id]);
|
|
}
|
|
|
|
await db.query('UPDATE system_modules SET is_active = false WHERE code = ?', [code]);
|
|
res.json({ success: true, message: 'Module deactivated' });
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: 'Database error' });
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// --- System Update Logic ---
|
|
|
|
const getGiteaAuth = async () => {
|
|
try {
|
|
const [rows] = await db.query("SELECT setting_key, setting_value FROM system_settings WHERE setting_key IN ('gitea_url', 'gitea_user', 'gitea_password')");
|
|
const settings = {};
|
|
rows.forEach(r => settings[r.setting_key] = r.setting_value);
|
|
|
|
const url = settings.gitea_url || 'https://gitea.qideun.com/SOKUREE/smart_ims.git';
|
|
|
|
if (settings.gitea_user && settings.gitea_password) {
|
|
const pass = cryptoUtil.decryptMasterKey(settings.gitea_password);
|
|
return { url, user: settings.gitea_user, pass: pass };
|
|
}
|
|
return { url, user: null, pass: null };
|
|
} catch (e) {
|
|
console.error('Failed to get Gitea auth:', e);
|
|
}
|
|
return { url: 'https://gitea.qideun.com/SOKUREE/smart_ims.git', user: null, pass: null };
|
|
};
|
|
|
|
// 5. Get Version Info (Current, Remote & History from Tags)
|
|
router.get('/version/remote', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
try {
|
|
const packageJsonPath = path.join(__dirname, '../package.json');
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
const currentVersion = packageJson.version.replace(/^v/, '');
|
|
|
|
// Prepare git fetch command with auth if available
|
|
const auth = await getGiteaAuth();
|
|
let fetchCmd = 'git fetch --tags --force';
|
|
|
|
if (auth.user && auth.pass) {
|
|
const authenticatedUrl = auth.url.replace('https://', `https://${encodeURIComponent(auth.user)}:${encodeURIComponent(auth.pass)}@`);
|
|
// Use explicit refspec to ensure local tags are updated from the remote URL
|
|
fetchCmd = `git fetch ${authenticatedUrl} +refs/tags/*:refs/tags/* --force --prune --prune-tags`;
|
|
} else {
|
|
fetchCmd = `git fetch ${auth.url} +refs/tags/*:refs/tags/* --force --prune --prune-tags`;
|
|
}
|
|
|
|
exec(fetchCmd, (err, stdout, stderr) => {
|
|
if (err) {
|
|
console.error('Git fetch failed:', err);
|
|
const sanitizedError = stderr.replace(/:[^@]+@/g, ':****@');
|
|
return res.json({
|
|
current: currentVersion,
|
|
latest: null,
|
|
history: [],
|
|
error: `원격 저장소 동기화 실패: ${sanitizedError || err.message}`
|
|
});
|
|
}
|
|
|
|
// Also ensure we are looking at the remote tags directly if possible for the 'latest' check
|
|
// but for history we still use the fetched local tags
|
|
const format = '%(refname:short)|%(contents:subject)|%(contents:body)|%(creatordate:iso8601)';
|
|
const historyCmd = `git for-each-ref refs/tags --sort=-creatordate --format="${format}" --count=10`;
|
|
|
|
exec(historyCmd, (err, stdout, stderr) => {
|
|
const lines = stdout ? stdout.trim().split('\n') : [];
|
|
const allTags = lines.map(line => {
|
|
const [tag, subject, body, date] = line.split('|');
|
|
if (!tag) return null;
|
|
|
|
let type = 'patch';
|
|
let title = subject || '';
|
|
const typeMatch = (subject || '').match(/^\[(URGENT|FEATURE|FIX|PATCH|HOTFIX)\]\s*(.*)$/i);
|
|
if (typeMatch) {
|
|
const rawType = typeMatch[1].toUpperCase();
|
|
if (rawType === 'URGENT' || rawType === 'HOTFIX') type = 'urgent';
|
|
else if (rawType === 'FEATURE') type = 'feature';
|
|
else type = 'fix';
|
|
title = typeMatch[2];
|
|
}
|
|
|
|
const changes = (body || '')
|
|
.split('\n')
|
|
.map(c => c.trim())
|
|
.filter(c => c.startsWith('-') || c.startsWith('*'))
|
|
.map(c => c.replace(/^[-*]\s*/, ''));
|
|
|
|
return {
|
|
version: tag.replace(/^v/, ''),
|
|
date: date ? date.split(' ')[0] : '',
|
|
title: title || '업데이트',
|
|
changes: changes.length > 0 ? changes : [subject || '세부 내역 없음'],
|
|
type: type
|
|
};
|
|
}).filter(Boolean);
|
|
|
|
const parseV = (v) => (v || '').replace(/^v/, '').split('.').map(n => parseInt(n) || 0);
|
|
const compare = (v1, v2) => {
|
|
const p1 = parseV(v1);
|
|
const p2 = parseV(v2);
|
|
for (let i = 0; i < 3; i++) {
|
|
const n1 = p1[i] || 0;
|
|
const n2 = p2[i] || 0;
|
|
if (n1 > n2) return 1;
|
|
if (n1 < n2) return -1;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
const history = allTags.filter(t => compare(t.version, currentVersion) <= 0);
|
|
const latestFromRemote = allTags[0] || null;
|
|
const needsUpdate = latestFromRemote ? (compare(latestFromRemote.version, currentVersion) > 0) : false;
|
|
|
|
res.json({
|
|
current: currentVersion,
|
|
latest: latestFromRemote ? `v${latestFromRemote.version}` : null,
|
|
needsUpdate: needsUpdate,
|
|
latestInfo: needsUpdate ? latestFromRemote : null,
|
|
history: history,
|
|
debug: { path: packageJsonPath }
|
|
});
|
|
});
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
res.status(500).json({ error: '버전 정보를 가져오는 중 오류가 발생했습니다.' });
|
|
}
|
|
});
|
|
|
|
// 6. Execute System Update
|
|
// WARNING: This is a heavy operation and will reload the server
|
|
router.post('/version/update', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
const { targetTag } = req.body;
|
|
|
|
if (!targetTag) {
|
|
return res.status(400).json({ error: '업데이트할 대상 태그가 지정되지 않았습니다.' });
|
|
}
|
|
|
|
// This operation is asynchronous. We start it and return a message.
|
|
// In a real production, we might want to log this to a terminal-like view.
|
|
|
|
const auth = await getGiteaAuth();
|
|
let authPrefix = '';
|
|
if (auth.user && auth.pass) {
|
|
const authenticatedUrl = auth.url.replace('https://', `https://${encodeURIComponent(auth.user)}:${encodeURIComponent(auth.pass)}@`);
|
|
authPrefix = `git remote set-url origin ${authenticatedUrl} && `;
|
|
} else {
|
|
authPrefix = `git remote set-url origin ${auth.url} && `;
|
|
}
|
|
|
|
const updateScript = `${authPrefix} git fetch --tags --force && git checkout -f ${targetTag} && npm install && npm run build && cd server && npm install && pm2 reload smartims-api`;
|
|
|
|
// Note: On Windows, use cmd.exe /c which supports '&&' better than default PowerShell
|
|
const isWindows = process.platform === 'win32';
|
|
const shellCommand = isWindows ? `cmd.exe /c "${updateScript}"` : updateScript;
|
|
|
|
console.log(`🚀 Starting system update to ${targetTag}...`);
|
|
console.log(`Executing: ${shellCommand}`);
|
|
|
|
exec(shellCommand, { cwd: path.join(__dirname, '../..') }, (err, stdout, stderr) => {
|
|
if (err) {
|
|
console.error('❌ Update Failed:', err);
|
|
// Sanitize output for logs
|
|
const sanitizedErr = stderr.replace(/:[^@]+@/g, ':****@');
|
|
console.error(sanitizedErr);
|
|
return;
|
|
}
|
|
console.log('✅ Update completed successfully.');
|
|
console.log(stdout);
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '업데이트 프로세스가 백그라운드에서 시작되었습니다. 약 1~3분 후 시스템이 재시작됩니다.'
|
|
});
|
|
});
|
|
|
|
module.exports = router;
|