- 최고관리자(Supervisor) 전용 2중 보안 잠금 시스템 및 인증 UI 적용 - 데이터베이스 인프라 및 암호화 마스터 키 자가 관리 기능 구축 - 권한 계층(Supervisor > Admin > User) 기반의 메뉴 노출 및 접근 제어 로직 강화 - 시스템 버전 정보 페이지 신규 추가 및 패키지 버전 자동 연동 (v0.2.5) - 사용자 관리 UI 디자인 개선 및 폰트/스타일 일원화
293 lines
21 KiB
Plaintext
293 lines
21 KiB
Plaintext
commit 7ee6446f0ff0452999a04d934e51eb1d9b46c2a9
|
|
Author: choibk <choibk350@gmail.com>
|
|
Date: Fri Jan 23 16:52:14 2026 +0900
|
|
|
|
?쇱씠?좎뒪 愿由?DB 遺꾨━
|
|
|
|
diff --git a/server/routes/auth.js b/server/routes/auth.js
|
|
index 8a4de01..e7be500 100644
|
|
--- a/server/routes/auth.js
|
|
+++ b/server/routes/auth.js
|
|
@@ -10,7 +10,7 @@ const { generateToken } = require('../middleware/csrfMiddleware');
|
|
// Key must be 32 bytes for aes-256-cbc
|
|
// 'my_super_secret_key_manage_asset' is 32 chars?
|
|
// let's use a simpler approach to ensure length on startup or fallback
|
|
-const SECRET_KEY = process.env.ENCRYPTION_KEY || 'smartasset_secret_key_0123456789'; // 32 chars needed
|
|
+const SECRET_KEY = process.env.ENCRYPTION_KEY || 'smartims_secret_key_0123456789'; // 32 chars needed
|
|
// Ideally use a buffer from hex, but string is okay if 32 chars.
|
|
// Let's pad it to ensure stability if env is missing.
|
|
const keyBuffer = crypto.scryptSync(SECRET_KEY, 'salt', 32);
|
|
@@ -127,7 +127,7 @@ router.post('/logout', (req, res) => {
|
|
console.error('Logout error:', err);
|
|
return res.status(500).json({ success: false, message: 'Logout failed' });
|
|
}
|
|
- res.clearCookie('smartasset_sid'); // matching key in index.js
|
|
+ res.clearCookie('smartims_sid'); // matching key in index.js
|
|
res.json({ success: true, message: 'Logged out' });
|
|
});
|
|
});
|
|
|
|
commit 5ead239b7141f17c0cf0bdbe11c76dd008fff900
|
|
Author: choibk <choibk350@gmail.com>
|
|
Date: Thu Jan 22 23:42:55 2026 +0900
|
|
|
|
Initial
|
|
|
|
diff --git a/server/routes/auth.js b/server/routes/auth.js
|
|
new file mode 100644
|
|
index 0000000..8a4de01
|
|
--- /dev/null
|
|
+++ b/server/routes/auth.js
|
|
@@ -0,0 +1,251 @@
|
|
+const express = require('express');
|
|
+const router = express.Router();
|
|
+const db = require('../db');
|
|
+const crypto = require('crypto');
|
|
+const { isAuthenticated, hasRole } = require('../middleware/authMiddleware');
|
|
+const { generateToken } = require('../middleware/csrfMiddleware');
|
|
+
|
|
+// --- Crypto Utilities ---
|
|
+// Use a fixed key for MVP. In production, store this securely in .env
|
|
+// Key must be 32 bytes for aes-256-cbc
|
|
+// 'my_super_secret_key_manage_asset' is 32 chars?
|
|
+// let's use a simpler approach to ensure length on startup or fallback
|
|
+const SECRET_KEY = process.env.ENCRYPTION_KEY || 'smartasset_secret_key_0123456789'; // 32 chars needed
|
|
+// Ideally use a buffer from hex, but string is okay if 32 chars.
|
|
+// Let's pad it to ensure stability if env is missing.
|
|
+const keyBuffer = crypto.scryptSync(SECRET_KEY, 'salt', 32);
|
|
+
|
|
+const ALGORITHM = 'aes-256-cbc';
|
|
+
|
|
+function encrypt(text) {
|
|
+ if (!text) return text;
|
|
+ const iv = crypto.randomBytes(16);
|
|
+ const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv);
|
|
+ let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
+ encrypted += cipher.final('hex');
|
|
+ return iv.toString('hex') + ':' + encrypted;
|
|
+}
|
|
+
|
|
+function decrypt(text) {
|
|
+ if (!text) return text;
|
|
+ // Check if it looks like our encrypted format (hexIV:hexContent)
|
|
+ if (!text.includes(':')) {
|
|
+ return text; // Assume plain text if no separator
|
|
+ }
|
|
+
|
|
+ try {
|
|
+ const textParts = text.split(':');
|
|
+ const ivHex = textParts.shift();
|
|
+
|
|
+ // IV for AES-256-CBC must be 16 bytes (32 hex characters)
|
|
+ if (!ivHex || ivHex.length !== 32) {
|
|
+ return text; // Invalid IV length, return original
|
|
+ }
|
|
+
|
|
+ const iv = Buffer.from(ivHex, 'hex');
|
|
+ const encryptedText = textParts.join(':');
|
|
+
|
|
+ const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv);
|
|
+ let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
|
|
+ decrypted += decipher.final('utf8');
|
|
+ return decrypted;
|
|
+ } catch (e) {
|
|
+ console.error('Decryption failed for:', text, e.message);
|
|
+ return text; // Return original if fail
|
|
+ }
|
|
+}
|
|
+
|
|
+function hashPassword(password) {
|
|
+ return crypto.createHash('sha256').update(password).digest('hex');
|
|
+}
|
|
+
|
|
+// --- Routes ---
|
|
+
|
|
+// 1. Login
|
|
+router.post('/login', async (req, res) => {
|
|
+ const { id, password } = req.body;
|
|
+ try {
|
|
+ const hashedPassword = hashPassword(password);
|
|
+ const [rows] = await db.query('SELECT * FROM users WHERE id = ? AND password = ?', [id, hashedPassword]);
|
|
+
|
|
+ if (rows.length > 0) {
|
|
+ const user = rows[0];
|
|
+ // Update last_login
|
|
+ await db.query('UPDATE users SET last_login = NOW() WHERE id = ?', [user.id]);
|
|
+
|
|
+ // Remove sensitive data
|
|
+ delete user.password;
|
|
+
|
|
+ // Should we decrypt phone? Maybe not needed for session, but let's decrypt just in case UI needs it
|
|
+ if (user.phone) user.phone = decrypt(user.phone);
|
|
+
|
|
+ // Save user to session
|
|
+ req.session.user = user;
|
|
+
|
|
+ // Generate CSRF Token
|
|
+ const csrfToken = generateToken();
|
|
+ req.session.csrfToken = csrfToken;
|
|
+
|
|
+ // Explicitly save session before response (optional but safer for race conditions)
|
|
+ req.session.save(err => {
|
|
+ if (err) {
|
|
+ console.error('Session save error:', err);
|
|
+ return res.status(500).json({ success: false, message: 'Session error' });
|
|
+ }
|
|
+ res.json({ success: true, user, csrfToken });
|
|
+ });
|
|
+ } else {
|
|
+ res.status(401).json({ success: false, message: 'Invalid credentials' });
|
|
+ }
|
|
+ } catch (err) {
|
|
+ console.error('Login error:', err);
|
|
+ res.status(500).json({ success: false, message: 'Server error' });
|
|
+ }
|
|
+});
|
|
+
|
|
+// 1.5. Check Session (New)
|
|
+router.get('/check', (req, res) => {
|
|
+ if (req.session.user) {
|
|
+ // Ensure CSRF token exists, if not generate one (edge case)
|
|
+ if (!req.session.csrfToken) {
|
|
+ req.session.csrfToken = generateToken();
|
|
+ }
|
|
+ res.json({
|
|
+ isAuthenticated: true,
|
|
+ user: req.session.user,
|
|
+ csrfToken: req.session.csrfToken
|
|
+ });
|
|
+ } else {
|
|
+ res.json({ isAuthenticated: false });
|
|
+ }
|
|
+});
|
|
+
|
|
+// 1.6. Logout (New)
|
|
+router.post('/logout', (req, res) => {
|
|
+ req.session.destroy(err => {
|
|
+ if (err) {
|
|
+ console.error('Logout error:', err);
|
|
+ return res.status(500).json({ success: false, message: 'Logout failed' });
|
|
+ }
|
|
+ res.clearCookie('smartasset_sid'); // matching key in index.js
|
|
+ res.json({ success: true, message: 'Logged out' });
|
|
+ });
|
|
+});
|
|
+
|
|
+// 2. List Users (Admin Only)
|
|
+router.get('/users', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
+ try {
|
|
+ // ideally check req.user.role if we had middleware, for now assuming client logic protection + internal/local usage
|
|
+ const [rows] = await db.query('SELECT id, name, department, position, phone, role, last_login, created_at, updated_at FROM users ORDER BY created_at DESC');
|
|
+
|
|
+ const users = rows.map(u => ({
|
|
+ ...u,
|
|
+ phone: decrypt(u.phone) // Decrypt phone for admin view
|
|
+ }));
|
|
+
|
|
+ res.json(users);
|
|
+ } catch (err) {
|
|
+ console.error(err);
|
|
+ res.status(500).json({ error: 'Database error' });
|
|
+ }
|
|
+});
|
|
+
|
|
+// 3. Create User
|
|
+router.post('/users', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
+ const { id, password, name, department, position, phone, role } = req.body;
|
|
+
|
|
+ if (!id || !password || !name) {
|
|
+ return res.status(400).json({ error: 'Missing required fields' });
|
|
+ }
|
|
+
|
|
+ try {
|
|
+ // Check if ID exists
|
|
+ const [existing] = await db.query('SELECT id FROM users WHERE id = ?', [id]);
|
|
+ if (existing.length > 0) {
|
|
+ return res.status(409).json({ error: 'User ID already exists' });
|
|
+ }
|
|
+
|
|
+ const hashedPassword = hashPassword(password);
|
|
+ const encryptedPhone = encrypt(phone);
|
|
+
|
|
+ const sql = `
|
|
+ INSERT INTO users (id, password, name, department, position, phone, role)
|
|
+ VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
+ `;
|
|
+
|
|
+ await db.query(sql, [id, hashedPassword, name, department, position, encryptedPhone, role || 'user']);
|
|
+
|
|
+ res.status(201).json({ message: 'User created' });
|
|
+ } catch (err) {
|
|
+ console.error(err);
|
|
+ res.status(500).json({ error: 'Database error' });
|
|
+ }
|
|
+});
|
|
+
|
|
+// 4. Update User
|
|
+router.put('/users/:id', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
+ const { password, name, department, position, phone, role } = req.body;
|
|
+ const userId = req.params.id;
|
|
+
|
|
+ try {
|
|
+ // Fetch current to keep old values if not provided? Frontend usually sends all.
|
|
+ // We update everything provided.
|
|
+
|
|
+ // Build query dynamically or just assume full update
|
|
+ let updates = [];
|
|
+ let params = [];
|
|
+
|
|
+ if (password) {
|
|
+ updates.push('password = ?');
|
|
+ params.push(hashPassword(password));
|
|
+ }
|
|
+ if (name) {
|
|
+ updates.push('name = ?');
|
|
+ params.push(name);
|
|
+ }
|
|
+ if (department !== undefined) {
|
|
+ updates.push('department = ?');
|
|
+ params.push(department);
|
|
+ }
|
|
+ if (position !== undefined) {
|
|
+ updates.push('position = ?');
|
|
+ params.push(position);
|
|
+ }
|
|
+ if (phone !== undefined) {
|
|
+ updates.push('phone = ?');
|
|
+ params.push(encrypt(phone));
|
|
+ }
|
|
+ if (role) {
|
|
+ updates.push('role = ?');
|
|
+ params.push(role);
|
|
+ }
|
|
+
|
|
+ if (updates.length === 0) return res.json({ message: 'No changes' });
|
|
+
|
|
+ const sql = `UPDATE users SET ${updates.join(', ')} WHERE id = ?`;
|
|
+ params.push(userId);
|
|
+
|
|
+ await db.query(sql, params);
|
|
+ res.json({ message: 'User updated' });
|
|
+
|
|
+ } catch (err) {
|
|
+ console.error(err);
|
|
+ res.status(500).json({ error: 'Database error' });
|
|
+ }
|
|
+});
|
|
+
|
|
+// 5. Delete User
|
|
+router.delete('/users/:id', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
+ try {
|
|
+ // Prevent deleting last admin? Optional.
|
|
+ // Prevent deleting self?
|
|
+
|
|
+ await db.query('DELETE FROM users WHERE id = ?', [req.params.id]);
|
|
+ res.json({ message: 'User deleted' });
|
|
+ } catch (err) {
|
|
+ console.error(err);
|
|
+ res.status(500).json({ error: 'Database error' });
|
|
+ }
|
|
+});
|
|
+
|
|
+module.exports = router;
|