smart_ims/auth_v1.js
choibk 8b2589b6fa feat: 플랫폼 보안 강화, 권한 계층 시스템 도입 및 버전 관리 통합 (v0.2.5)
- 최고관리자(Supervisor) 전용 2중 보안 잠금 시스템 및 인증 UI 적용
- 데이터베이스 인프라 및 암호화 마스터 키 자가 관리 기능 구축
- 권한 계층(Supervisor > Admin > User) 기반의 메뉴 노출 및 접근 제어 로직 강화
- 시스템 버전 정보 페이지 신규 추가 및 패키지 버전 자동 연동 (v0.2.5)
- 사용자 관리 UI 디자인 개선 및 폰트/스타일 일원화
2026-01-24 17:17:33 +09:00

252 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;