2026-01-23 16:52:14 +09:00

252 lines
8.3 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');
// --- 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 || '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);
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('smartims_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;