Files
backmanager-server/routes/users.js
2026-06-25 21:18:13 +08:00

420 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// routes/users.js —— 用户登录 / 信息 / CRUD
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const { pool } = require('../db')
// 安全字段:关联查询 roles 和 employees
const USER_LIST_SQL = `
SELECT u.id, u.username, u.is_active, u.role_id, u.employee_id, u.department,
u.created_at, u.updated_at,
r.name AS role_name, r.description AS role_description,
e.name AS real_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
LEFT JOIN employees e ON u.employee_id = e.id
`
function pagination(query) {
const page = Math.max(Number(query.page) || 1, 1)
const pageSize = Math.min(Math.max(Number(query.pageSize) || 10, 1), 100)
const offset = (page - 1) * pageSize
return { page, pageSize, offset }
}
// ========== 登录 / 登出 / 个人信息 ==========
// POST /api/user/login —— 登录(无需 token
async function login(req, res) {
const { username, password } = req.body || {}
if (!username || !password) {
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
}
try {
const [rows] = await pool.query(
`SELECT u.id, u.username, u.password, u.is_active, u.role_id, u.department, u.employee_id,
r.name AS role_name, e.name AS real_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
LEFT JOIN employees e ON u.employee_id = e.id
WHERE u.username = ?`,
[username]
)
if (rows.length === 0) {
return res.status(400).json({ code: 400, message: '账号或密码错误' })
}
const user = rows[0]
// 检查账号是否被禁用
if (user.is_active === 0) {
return res.status(403).json({ code: 403, message: '账号已被禁用,请联系管理员' })
}
const ok = await bcrypt.compare(password, user.password)
if (!ok) {
return res.status(400).json({ code: 400, message: '账号或密码错误' })
}
// 查询该角色的所有权限标识
const [perms] = await pool.query(
`SELECT p.name FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
WHERE rp.role_id = ?`,
[user.role_id]
)
const permissions = perms.map(p => p.name)
const token = jwt.sign(
{
id: user.id,
username: user.username,
name: user.real_name || user.username,
role_id: user.role_id,
roleName: user.role_name,
department: user.department,
permissions,
},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '2h' }
)
res.status(200).json({
code: 200,
message: 'ok',
data: {
token,
userInfo: {
id: user.id,
username: user.username,
name: user.real_name || user.username,
role_id: user.role_id,
roleName: user.role_name,
department: user.department,
permissions,
},
},
})
} catch (e) {
console.error('[login] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/user/info —— 获取当前登录用户信息(需要 token
async function info(req, res) {
try {
const [rows] = await pool.query(
`${USER_LIST_SQL} WHERE u.id = ?`,
[req.user.id]
)
if (rows.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[info] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/user/list —— 简易用户列表(用于下拉选择负责人,仅需登录)
async function simpleList(req, res) {
try {
const [rows] = await pool.query(
`SELECT u.id, u.username, e.name AS real_name
FROM users u LEFT JOIN employees e ON u.employee_id = e.id
WHERE u.is_active = 1 ORDER BY u.id`
)
res.json({ code: 0, message: 'ok', data: rows })
} catch (e) {
console.error('[user simpleList] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/user/logout —— 登出(需要 token仅做应答
async function logout(req, res) {
res.json({ code: 0, message: 'ok' })
}
// PUT /api/user/password —— 当前用户修改自己的密码(需要 token
async function changePassword(req, res) {
const { oldPassword, newPassword } = req.body || {}
if (!oldPassword || !newPassword) {
return res.status(400).json({ code: 400, message: '旧密码和新密码必填' })
}
if (typeof newPassword !== 'string' || newPassword.length < 6) {
return res.status(400).json({ code: 400, message: '新密码至少 6 位' })
}
if (oldPassword === newPassword) {
return res.status(400).json({ code: 400, message: '新密码不能与旧密码相同' })
}
try {
const [rows] = await pool.query('SELECT password FROM users WHERE id = ?', [req.user.id])
if (rows.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
const ok = await bcrypt.compare(oldPassword, rows[0].password)
if (!ok) {
return res.status(400).json({ code: 400, message: '旧密码错误' })
}
const hash = await bcrypt.hash(newPassword, 10)
await pool.query('UPDATE users SET password = ? WHERE id = ?', [hash, req.user.id])
res.json({ code: 0, message: '密码修改成功' })
} catch (e) {
console.error('[changePassword] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// ========== 用户管理 CRUD管理员 ==========
// GET /api/users —— 用户列表
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { username, real_name, id, role_id, is_active } = req.query
let where = 'WHERE 1=1'
const params = []
if (username) {
where += ' AND u.username LIKE ?'
params.push(`%${username}%`)
}
if (real_name) {
where += ' AND e.name LIKE ?'
params.push(`%${real_name}%`)
}
if (id) {
where += ' AND u.id = ?'
params.push(Number(id))
}
if (role_id) {
where += ' AND u.role_id = ?'
params.push(Number(role_id))
}
if (is_active !== undefined && is_active !== '') {
where += ' AND u.is_active = ?'
params.push(Number(is_active))
}
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM users u
LEFT JOIN employees e ON u.employee_id = e.id
${where}`,
params
)
const [rows] = await pool.query(
`${USER_LIST_SQL} ${where} ORDER BY u.id DESC LIMIT ? OFFSET ?`,
[...params, pageSize, offset]
)
res.json({
code: 0,
message: 'ok',
data: {
list: rows,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
},
})
} catch (e) {
console.error('[users list] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/users/:id —— 用户详情
async function detail(req, res) {
try {
const [rows] = await pool.query(
`${USER_LIST_SQL} WHERE u.id = ?`,
[req.params.id]
)
if (rows.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[users detail] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/users —— 创建用户
async function create(req, res) {
const { username, password, role_id, employee_id, department, is_active = 1 } = req.body || {}
if (!username || !password) {
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
}
if (typeof username !== 'string' || username.length < 3 || username.length > 50) {
return res.status(400).json({ code: 400, message: '用户名长度需在 3-50 之间' })
}
if (typeof password !== 'string' || password.length < 6) {
return res.status(400).json({ code: 400, message: '密码至少 6 位' })
}
// 校验 role_id 是否存在
if (role_id) {
const [role] = await pool.query('SELECT id FROM roles WHERE id = ?', [role_id])
if (role.length === 0) {
return res.status(400).json({ code: 400, message: '指定的角色不存在' })
}
}
// 校验 employee_id 是否存在
if (employee_id) {
const [emp] = await pool.query('SELECT id, department FROM employees WHERE id = ?', [employee_id])
if (emp.length === 0) {
return res.status(400).json({ code: 400, message: '指定的员工不存在' })
}
// 如果没传 department自动从员工表同步
if (!department) {
req.body.department = emp[0].department
}
}
try {
const hash = await bcrypt.hash(password, 10)
const [result] = await pool.query(
'INSERT INTO users (username, password, is_active, role_id, employee_id, department) VALUES (?, ?, ?, ?, ?, ?)',
[username, hash, is_active, role_id || null, employee_id || null, req.body.department || department || null]
)
const [rows] = await pool.query(
`${USER_LIST_SQL} WHERE u.id = ?`,
[result.insertId]
)
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ code: 409, message: '用户名已存在' })
}
console.error('[users create] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// PUT /api/users/:id —— 更新用户
async function update(req, res) {
const { id } = req.params
const fields = ['username', 'role_id', 'employee_id', 'department', 'is_active']
try {
const [existing] = await pool.query('SELECT id, role_id, is_active FROM users WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
// 不允许修改自己的角色或禁用自己
if (Number(id) === req.user.id) {
if (req.body.role_id !== undefined && req.body.role_id !== req.user.role_id) {
return res.status(400).json({ code: 400, message: '不能修改自己的角色' })
}
if (req.body.is_active === 0) {
return res.status(400).json({ code: 400, message: '不能禁用自己' })
}
}
// 校验 role_id
if (req.body.role_id) {
const [role] = await pool.query('SELECT id FROM roles WHERE id = ?', [req.body.role_id])
if (role.length === 0) {
return res.status(400).json({ code: 400, message: '指定的角色不存在' })
}
}
// 校验 employee_id
if (req.body.employee_id) {
const [emp] = await pool.query('SELECT id, department FROM employees WHERE id = ?', [req.body.employee_id])
if (emp.length === 0) {
return res.status(400).json({ code: 400, message: '指定的员工不存在' })
}
// 自动同步 department
if (req.body.department === undefined) {
req.body.department = emp[0].department
}
}
const sets = []
const params = []
for (const f of fields) {
if (req.body[f] !== undefined) {
sets.push(`${f} = ?`)
params.push(req.body[f])
}
}
// 密码单独处理
if (req.body.password !== undefined) {
if (typeof req.body.password !== 'string' || req.body.password.length < 6) {
return res.status(400).json({ code: 400, message: '密码至少 6 位' })
}
const hash = await bcrypt.hash(req.body.password, 10)
sets.push('password = ?')
params.push(hash)
}
if (sets.length === 0) {
return res.status(400).json({ code: 400, message: '没有需要更新的字段' })
}
params.push(id)
await pool.query(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`, params)
const [rows] = await pool.query(
`${USER_LIST_SQL} WHERE u.id = ?`,
[id]
)
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ code: 409, message: '用户名已存在' })
}
console.error('[users update] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// DELETE /api/users/:id —— 删除用户
async function remove(req, res) {
const { id } = req.params
try {
if (Number(id) === req.user.id) {
return res.status(400).json({ code: 400, message: '不能删除自己' })
}
const [existing] = await pool.query('SELECT id, role_id FROM users WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
// 禁止删除最后一个管理员
const [adminRole] = await pool.query('SELECT id FROM roles WHERE name = ?', ['admin'])
if (adminRole.length > 0 && existing[0].role_id === adminRole[0].id) {
const [[{ cnt }]] = await pool.query('SELECT COUNT(*) AS cnt FROM users WHERE role_id = ?', [adminRole[0].id])
if (cnt <= 1) {
return res.status(400).json({ code: 400, message: '不能删除最后一个管理员' })
}
}
await pool.query('DELETE FROM users WHERE id = ?', [id])
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[users delete] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
module.exports = { login, info, simpleList, logout, changePassword, list, detail, create, update, remove }