diff --git a/server/check_sub.js b/server/check_sub.js new file mode 100644 index 0000000..cfeb6b7 --- /dev/null +++ b/server/check_sub.js @@ -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(); diff --git a/server/debug_db.js b/server/debug_db.js new file mode 100644 index 0000000..de87e37 --- /dev/null +++ b/server/debug_db.js @@ -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(); diff --git a/server/index.js b/server/index.js index 2e67dd1..e3ac82a 100644 --- a/server/index.js +++ b/server/index.js @@ -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); } diff --git a/server/middleware/csrfMiddleware.js b/server/middleware/csrfMiddleware.js index 3ad103f..785efeb 100644 --- a/server/middleware/csrfMiddleware.js +++ b/server/middleware/csrfMiddleware.js @@ -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(); diff --git a/server/package-lock.json b/server/package-lock.json index edf362f..8688b4d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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", diff --git a/server/package.json b/server/package.json index ab40139..bb05b90 100644 --- a/server/package.json +++ b/server/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/server/reset_admin.js b/server/reset_admin.js new file mode 100644 index 0000000..39f5e8f --- /dev/null +++ b/server/reset_admin.js @@ -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(); diff --git a/server/routes/system.js b/server/routes/system.js index 6e38e79..b6a1ec5 100644 --- a/server/routes/system.js +++ b/server/routes/system.js @@ -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 + }); } }); diff --git a/src/platform/pages/BasicSettingsPage.tsx b/src/platform/pages/BasicSettingsPage.tsx index a52fffd..edb095a 100644 --- a/src/platform/pages/BasicSettingsPage.tsx +++ b/src/platform/pages/BasicSettingsPage.tsx @@ -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() {
- -

- - 식별 정보 -

-
-
- - setSettings({ ...settings, subscriber_id: e.target.value })} - placeholder="예: SOKR-2024-001" - /> -

λΌμ΄μ„ μŠ€ ν™œμ„±ν™” μ‹œ μ‚¬μš©λ˜λŠ” μ„œλ²„ 고유 μ‹λ³„μžμž…λ‹ˆλ‹€.

-
-
-
+

diff --git a/src/system/pages/LicensePage.tsx b/src/system/pages/LicensePage.tsx index bcd2336..a63f45d 100644 --- a/src/system/pages/LicensePage.tsx +++ b/src/system/pages/LicensePage.tsx @@ -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(null); - const [historyData, setHistoryData] = useState([]); - const fetchHistory = async (code: string) => { - try { - const res = await apiClient.get(`/system/modules/${code}/history`); - setHistoryData(res.data); - setHistoryModule(code); - } catch (err) { - alert('이λ ₯을 λΆˆλŸ¬μ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); - } - }; return (
@@ -109,35 +97,36 @@ export function LicensePage() { λΌμ΄μ„ μŠ€ 및 λͺ¨λ“ˆ 관리

- {/* Server Configuration Section */}

μ„œλ²„ ν™˜κ²½ μ„€μ •

-
-
- - setInputSubscriberId(e.target.value)} - /> -

- * λΌμ΄μ„ μŠ€ 킀에 ν¬ν•¨λœ ID와 μΌμΉ˜ν•΄μ•Ό ν™œμ„±ν™”λ©λ‹ˆλ‹€. -

+
+
+
+ + setInputSubscriberId(e.target.value)} + /> +
+
- +

+ * λΌμ΄μ„ μŠ€ 킀에 ν¬ν•¨λœ ID와 μΌμΉ˜ν•΄μ•Ό ν™œμ„±ν™”λ©λ‹ˆλ‹€. +

@@ -193,10 +182,10 @@ export function LicensePage() { ) : (
)} - {/* History Modal */} - {historyModule && ( -
-
-

λΌμ΄μ„ μŠ€ λ³€κ²½ 이λ ₯ ({moduleInfo[historyModule]?.title})

-
- - - - - - - - - - - {historyData.length === 0 ? ( - - - - ) : ( - historyData.map((h: any) => ( - - - - - - - )) - )} - -
ꡐ체 μΌμ‹œκ΅¬λ…μžμœ ν˜•λΌμ΄μ„ μŠ€ ν‚€ (일뢀)
이λ ₯이 μ—†μŠ΅λ‹ˆλ‹€.
{new Date(h.activated_at).toLocaleString()}{h.subscriber_id || 'N/A'}{h.license_type || 'N/A'} - {h.license_key ? h.license_key.substring(0, 20) + '...' : '-'} -
-
- -
- -
-
-
- )}
);