284 lines
10 KiB
JavaScript
284 lines
10 KiB
JavaScript
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');
|
|
|
|
const cryptoUtil = require('../utils/cryptoUtil');
|
|
|
|
async function encrypt(text) {
|
|
return await cryptoUtil.encrypt(text);
|
|
}
|
|
|
|
async function decrypt(text) {
|
|
return await cryptoUtil.decrypt(text);
|
|
}
|
|
|
|
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 = await 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
|
|
router.get('/check', async (req, res) => {
|
|
try {
|
|
if (req.session.user) {
|
|
// Ensure CSRF token exists
|
|
if (!req.session.csrfToken) {
|
|
req.session.csrfToken = generateToken();
|
|
}
|
|
|
|
// Priority: User's individual timeout > System default
|
|
let timeoutMinutes = 60;
|
|
if (req.session.user.session_timeout) {
|
|
timeoutMinutes = parseInt(req.session.user.session_timeout);
|
|
} else {
|
|
const [rows] = await db.query("SELECT setting_value FROM system_settings WHERE setting_key = 'session_timeout'");
|
|
timeoutMinutes = rows.length > 0 ? parseInt(rows[0].setting_value) : 10;
|
|
}
|
|
|
|
res.json({
|
|
isAuthenticated: true,
|
|
user: req.session.user,
|
|
csrfToken: req.session.csrfToken,
|
|
sessionTimeout: timeoutMinutes * 60 * 1000
|
|
});
|
|
} else {
|
|
res.json({ isAuthenticated: false });
|
|
}
|
|
} catch (err) {
|
|
console.error('Check session error:', err);
|
|
res.status(500).json({ success: false, message: 'Server error' });
|
|
}
|
|
});
|
|
|
|
// 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('smartims_sid'); // matching key in index.js
|
|
res.json({ success: true, message: 'Logged out' });
|
|
});
|
|
});
|
|
|
|
// 1.7 Verify Supervisor (For sensitive settings)
|
|
router.post('/verify-supervisor', isAuthenticated, async (req, res) => {
|
|
const { password } = req.body;
|
|
if (!password) return res.status(400).json({ error: 'Password required' });
|
|
|
|
try {
|
|
if (req.session.user.role !== 'supervisor') {
|
|
return res.status(403).json({ error: '권한이 없습니다. 최고관리자만 접근 가능합니다.' });
|
|
}
|
|
|
|
const hashedPassword = hashPassword(password);
|
|
const [rows] = await db.query('SELECT 1 FROM users WHERE id = ? AND password = ?', [req.session.user.id, hashedPassword]);
|
|
|
|
if (rows.length > 0) {
|
|
res.json({ success: true, message: 'Verification successful' });
|
|
} else {
|
|
res.status(401).json({ success: false, message: '비밀번호가 일치하지 않습니다.' });
|
|
}
|
|
} catch (err) {
|
|
console.error('Verify supervisor error:', err);
|
|
res.status(500).json({ error: '인증 처리 중 오류가 발생했습니다.' });
|
|
}
|
|
});
|
|
|
|
// 2. List Users (Admin Only)
|
|
router.get('/users', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
try {
|
|
const [rows] = await db.query('SELECT id, name, department, position, phone, role, session_timeout, last_login, created_at, updated_at FROM users ORDER BY created_at DESC');
|
|
|
|
if (!rows || rows.length === 0) {
|
|
return res.json([]);
|
|
}
|
|
|
|
// Use Promise.all for safe async decryption
|
|
const users = await Promise.all(rows.map(async (u) => {
|
|
const decryptedPhone = await decrypt(u.phone);
|
|
return {
|
|
...u,
|
|
phone: decryptedPhone
|
|
};
|
|
}));
|
|
|
|
res.json(users);
|
|
} catch (err) {
|
|
console.error('Failed to list users:', err);
|
|
res.status(500).json({ error: '데이터를 불러오는 중 오류가 발생했습니다.' });
|
|
}
|
|
});
|
|
|
|
// 3. Create User
|
|
router.post('/users', isAuthenticated, hasRole('admin'), async (req, res) => {
|
|
const { id, password, name, department, position, phone, role, session_timeout } = 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 = await encrypt(phone);
|
|
|
|
const sql = `
|
|
INSERT INTO users (id, password, name, department, position, phone, role, session_timeout)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`;
|
|
|
|
await db.query(sql, [id, hashedPassword, name, department, position, encryptedPhone, role || 'user', session_timeout || 10]);
|
|
|
|
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, session_timeout } = req.body;
|
|
const userId = req.params.id;
|
|
|
|
try {
|
|
// ... (existing logic)
|
|
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(await encrypt(phone)); }
|
|
if (role) { updates.push('role = ?'); params.push(role); }
|
|
if (session_timeout !== undefined) { updates.push('session_timeout = ?'); params.push(session_timeout); }
|
|
|
|
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' });
|
|
}
|
|
});
|
|
|
|
// 4.5. Update My Profile (New)
|
|
router.put('/profile', isAuthenticated, async (req, res) => {
|
|
const { name, phone, session_timeout } = req.body;
|
|
const userId = req.session.user.id;
|
|
|
|
try {
|
|
let updates = [];
|
|
let params = [];
|
|
|
|
if (name) {
|
|
updates.push('name = ?');
|
|
params.push(name);
|
|
}
|
|
if (phone !== undefined) {
|
|
updates.push('phone = ?');
|
|
params.push(await encrypt(phone));
|
|
}
|
|
if (session_timeout !== undefined) {
|
|
updates.push('session_timeout = ?');
|
|
params.push(session_timeout);
|
|
}
|
|
|
|
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);
|
|
|
|
// Update session user object
|
|
const [rows] = await db.query('SELECT id, name, department, position, phone, role, session_timeout FROM users WHERE id = ?', [userId]);
|
|
req.session.user = rows[0];
|
|
|
|
// Also update cookie maxAge if timeout changed
|
|
if (session_timeout) {
|
|
req.session.cookie.maxAge = parseInt(session_timeout) * 60 * 1000;
|
|
}
|
|
|
|
req.session.save(() => {
|
|
res.json({ message: 'Profile updated', user: req.session.user });
|
|
});
|
|
} 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;
|