chore: save point before architectural refactoring for modularity

This commit is contained in:
choibk 2026-01-23 23:06:20 +09:00
parent 7ee6446f0f
commit 59e876c838
10 changed files with 293 additions and 118 deletions

26
server/check_sub.js Normal file
View File

@ -0,0 +1,26 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function check() {
try {
const db = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: process.env.DB_PORT || 3307
});
const [rows] = await db.query("SELECT * FROM system_settings WHERE setting_key = 'subscriber_id'");
if (rows.length > 0) {
console.log('Subscriber ID Value: [' + rows[0].setting_value + ']');
console.log('Length:', rows[0].setting_value.length);
} else {
console.log('Subscriber ID NOT SET');
}
process.exit(0);
} catch (err) {
console.error(err);
process.exit(1);
}
}
check();

27
server/debug_db.js Normal file
View File

@ -0,0 +1,27 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
async function check() {
try {
const db = await mysql.createConnection({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'smartims_db',
port: process.env.DB_PORT || 3307
});
const [rows] = await db.query("SELECT * FROM system_settings");
console.log('--- System Settings ---');
console.log(JSON.stringify(rows, null, 2));
const [modules] = await db.query("SELECT * FROM system_modules");
console.log('--- System Modules ---');
console.log(JSON.stringify(modules, null, 2));
process.exit(0);
} catch (err) {
console.error(err);
process.exit(1);
}
}
check();

View File

@ -54,8 +54,9 @@ const sessionStore = new MySQLStore(sessionStoreOptions);
// Middleware
app.use(cors({
origin: true, // Allow all origins (or specific one)
credentials: true // Important for cookies
origin: true,
credentials: true,
allowedHeaders: ['Content-Type', 'X-CSRF-Token', 'Authorization']
}));
app.use(express.json());
app.use('/uploads', express.static(uploadDir));
@ -65,7 +66,7 @@ app.use(session({
key: 'smartims_sid',
secret: process.env.SESSION_SECRET || 'smartims_session_secret_key',
store: sessionStore,
resave: false,
resave: true, // Force save to avoid session loss in some environments
saveUninitialized: false,
cookie: {
httpOnly: true,
@ -82,6 +83,9 @@ app.use(async (req, res, next) => {
const [rows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'session_timeout'");
const timeoutMinutes = rows.length > 0 ? parseInt(rows[0].setting_value) : 60;
req.session.cookie.maxAge = timeoutMinutes * 60 * 1000;
// Explicitly save session to ensure store sync
req.session.save();
} catch (err) {
console.error('Session timeout fetch error:', err);
}

View File

@ -12,7 +12,7 @@ const csrfProtection = (req, res, next) => {
return next();
}
// Skip for Login endpoint (initial session creation)
// Skip for Login endpoint
if (req.path === '/api/login' || req.path === '/api/auth/login') {
return next();
}
@ -24,8 +24,18 @@ const csrfProtection = (req, res, next) => {
const tokenFromSession = req.session.csrfToken;
if (!tokenFromSession || !tokenFromHeader || tokenFromSession !== tokenFromHeader) {
console.error('CSRF mismatch:', { session: tokenFromSession, header: tokenFromHeader });
return res.status(403).json({ success: false, message: 'Invalid CSRF Token' });
console.error('🔒 [CSRF Security] Token Mismatch Detected');
console.error(`- Path: ${req.path}`);
console.error(`- Session ID: ${req.sessionID ? req.sessionID.substring(0, 8) + '...' : 'NONE'}`);
console.error(`- Session User: ${req.session?.user?.id || 'GUEST'}`);
console.error(`- Token in Session: ${tokenFromSession ? 'EXISTS (' + tokenFromSession.substring(0, 5) + '...)' : 'MISSING'}`);
console.error(`- Token in Header: ${tokenFromHeader ? 'EXISTS (' + tokenFromHeader.substring(0, 5) + '...)' : 'MISSING'}`);
return res.status(403).json({
success: false,
message: 'Invalid CSRF Token',
error: '세션 보안 토큰(CSRF)이 일치하지 않습니다. 보안 정책에 따라 요청이 거부되었습니다. 다시 로그인하거나 새로고침 후 시도해 주세요.'
});
}
next();

132
server/package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.1.0",
"license": "ISC",
"dependencies": {
"axios": "^1.13.2",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
@ -95,6 +96,12 @@
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
@ -104,6 +111,17 @@
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -258,6 +276,18 @@
"fsevents": "~2.3.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -350,6 +380,15 @@
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -448,6 +487,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -674,6 +728,63 @@
"node": ">=18"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -809,6 +920,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -1341,6 +1467,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",

View File

@ -13,6 +13,7 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
"axios": "^1.13.2",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
@ -27,4 +28,4 @@
"devDependencies": {
"nodemon": "^3.1.11"
}
}
}

35
server/reset_admin.js Normal file
View File

@ -0,0 +1,35 @@
const db = require('./db');
const crypto = require('crypto');
async function resetAdmin() {
try {
const adminId = 'admin';
const password = 'admin123';
const hashedPassword = crypto.createHash('sha256').update(password).digest('hex');
console.log(`Resetting password for ${adminId}...`);
console.log(`New Hash: ${hashedPassword}`);
const [result] = await db.query(
'UPDATE users SET password = ? WHERE id = ?',
[hashedPassword, adminId]
);
if (result.affectedRows > 0) {
console.log('✅ Admin password reset successfully to "admin123"');
} else {
console.log('❌ Admin user not found. Creating new admin...');
await db.query(
'INSERT INTO users (id, password, name, role, department, position) VALUES (?, ?, ?, ?, ?, ?)',
[adminId, hashedPassword, '시스템 관리자', 'admin', 'IT팀', '관리자']
);
console.log('✅ Default Admin Created (admin / admin123)');
}
process.exit(0);
} catch (err) {
console.error('❌ Reset Failed:', err);
process.exit(1);
}
}
resetAdmin();

View File

@ -13,11 +13,12 @@ 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 in server root. License verification will fail.');
console.error('❌ WARNING: public_key.pem not found at:', publicKeyPath);
}
} catch (e) {
console.error('Error loading public key:', e);
console.error('Error loading public key:', e);
}
// 0. Server Configuration (Subscriber ID & Session Timeout)
@ -87,6 +88,8 @@ router.get('/modules', isAuthenticated, async (req, res) => {
}
});
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) => {
@ -133,8 +136,6 @@ router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async
INSERT INTO license_history (module_code, license_key, license_type, subscriber_id, activated_at)
VALUES (?, ?, ?, ?, NOW())
`;
// Store "activated_at" as roughly the time it was active until now (or simply log the event time)
// Actually, let's treat "activated_at" in history as "when it was archived/replaced".
await db.query(historySql, [old.code, old.license_key, old.license_type, old.subscriber_id]);
}
@ -159,11 +160,25 @@ router.post('/modules/:code/activate', isAuthenticated, hasRole('admin'), async
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) {
console.error('⚠️ Failed to sync status with License Manager:', syncErr.message);
// 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(err);
res.status(500).json({ error: 'Database error' });
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
});
}
});

View File

@ -7,7 +7,6 @@ import { Save, Clock, Info } from 'lucide-react';
export function BasicSettingsPage() {
const [settings, setSettings] = useState({
subscriber_id: '',
session_timeout: 60
});
const [loading, setLoading] = useState(true);
@ -51,23 +50,7 @@ export function BasicSettingsPage() {
</div>
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Info size={20} className="text-indigo-500" />
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1"> ID ( )</label>
<Input
value={settings.subscriber_id}
onChange={e => setSettings({ ...settings, subscriber_id: e.target.value })}
placeholder="예: SOKR-2024-001"
/>
<p className="text-xs text-slate-400 mt-1"> .</p>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">

View File

@ -28,7 +28,7 @@ export function LicensePage() {
async function fetchSubscriberId() {
try {
const res = await apiClient.get('/system/config');
const res = await apiClient.get('/system/settings');
setServerSubscriberId(res.data.subscriber_id || '');
setInputSubscriberId(res.data.subscriber_id || '');
} catch (err) {
@ -40,7 +40,7 @@ export function LicensePage() {
if (!inputSubscriberId.trim()) return alert('구독자 ID를 입력해주세요.');
setIsConfigSaving(true);
try {
await apiClient.post('/system/config', { subscriber_id: inputSubscriberId });
await apiClient.post('/system/settings', { subscriber_id: inputSubscriberId });
setServerSubscriberId(inputSubscriberId);
alert('구독자 ID가 저장되었습니다.');
} catch (err) {
@ -88,19 +88,7 @@ export function LicensePage() {
// ... Generator logic omitted for brevity as it's dev-only ...
// History State
const [historyModule, setHistoryModule] = useState<string | null>(null);
const [historyData, setHistoryData] = useState<any[]>([]);
const fetchHistory = async (code: string) => {
try {
const res = await apiClient.get(`/system/modules/${code}/history`);
setHistoryData(res.data);
setHistoryModule(code);
} catch (err) {
alert('이력을 불러오는데 실패했습니다.');
}
};
return (
<div className="space-y-6">
@ -109,35 +97,36 @@ export function LicensePage() {
</h2>
{/* Server Configuration Section */}
<div className="bg-slate-50 border border-slate-200 rounded-lg p-6 mb-8">
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
<Terminal size={20} className="text-slate-600" />
</h3>
<div className="flex items-end gap-4 max-w-xl">
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 mb-1">
ID (Server Subscriber ID)
</label>
<input
type="text"
className="w-full border border-slate-300 rounded px-3 py-2"
placeholder="예: SAMSUNG, DEMO_USER_01"
value={inputSubscriberId}
onChange={(e) => setInputSubscriberId(e.target.value)}
/>
<p className="text-xs text-slate-500 mt-1">
* ID와 .
</p>
<div className="max-w-xl">
<div className="flex items-end gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 mb-1">
ID (Server Subscriber ID)
</label>
<input
type="text"
className="w-full border border-slate-300 rounded px-3 py-2"
placeholder="예: SAMSUNG, DEMO_USER_01"
value={inputSubscriberId}
onChange={(e) => setInputSubscriberId(e.target.value)}
/>
</div>
<button
onClick={handleSaveConfig}
disabled={isConfigSaving}
className="bg-slate-800 text-white px-6 py-2 rounded hover:bg-slate-900 transition disabled:opacity-50 h-[42px]"
>
{isConfigSaving ? '저장 중...' : '설정 저장'}
</button>
</div>
<button
onClick={handleSaveConfig}
disabled={isConfigSaving}
className="bg-slate-800 text-white px-4 py-2 rounded hover:bg-slate-900 transition disabled:opacity-50"
>
{isConfigSaving ? '저장 중...' : '설정 저장'}
</button>
<p className="text-xs text-slate-500 mt-1">
* ID와 .
</p>
</div>
</div>
@ -193,10 +182,10 @@ export function LicensePage() {
) : (
<div className="flex gap-2">
<button
onClick={() => fetchHistory(code)}
className="flex-1 bg-gray-100 text-gray-700 border border-gray-200 py-2 rounded hover:bg-gray-200 transition text-sm"
onClick={() => setSelectedModule(code)}
className="flex-1 bg-blue-50 text-blue-600 border border-blue-100 py-2 rounded hover:bg-blue-100 transition text-sm flex items-center justify-center gap-1"
>
<Key size={14} />
</button>
<button
onClick={() => handleDeactivate(code)}
@ -260,54 +249,7 @@ export function LicensePage() {
</div>
)}
{/* History Modal */}
{historyModule && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-2xl">
<h3 className="text-lg font-bold mb-4"> ({moduleInfo[historyModule]?.title})</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm text-left border-collapse">
<thead className="bg-gray-50 text-gray-700">
<tr>
<th className="p-2 border-b"> </th>
<th className="p-2 border-b"></th>
<th className="p-2 border-b"></th>
<th className="p-2 border-b"> ()</th>
</tr>
</thead>
<tbody>
{historyData.length === 0 ? (
<tr>
<td colSpan={4} className="p-4 text-center text-gray-500"> .</td>
</tr>
) : (
historyData.map((h: any) => (
<tr key={h.id} className="border-b">
<td className="p-2">{new Date(h.activated_at).toLocaleString()}</td>
<td className="p-2">{h.subscriber_id || 'N/A'}</td>
<td className="p-2 uppercase">{h.license_type || 'N/A'}</td>
<td className="p-2 font-mono text-gray-500 text-xs">
{h.license_key ? h.license_key.substring(0, 20) + '...' : '-'}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
<div className="flex justify-end mt-4">
<button
onClick={() => setHistoryModule(null)}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
>
</button>
</div>
</div>
</div>
)}
</div>
);