commit 7ee6446f0ff0452999a04d934e51eb1d9b46c2a9 Author: choibk 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 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;