Files
backmanager-server/server.js
2026-06-22 19:02:46 +08:00

174 lines
5.7 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.
// server.js —— 启动服务 + 鉴权中间件 + 3 个接口
require('dotenv').config()
const express = require('express')
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const { pool, initDB } = require('./db')
const app = express()
app.use(express.json()) // 解析 application/json 请求体
// ============ 鉴权中间件 ============
// 验证请求头里的 Bearer token把解析出来的用户挂到 req.user
function auth(req, res, next) {
const header = req.headers.authorization || ''
const token = header.replace(/^Bearer\s+/i, '')
if (!token) {
return res.status(401).json({ code: 401, message: '未登录' })
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET)
next()
} catch (e) {
return res.status(401).json({ code: 401, message: 'token 无效或已过期' })
}
}
// ============ 管理员校验中间件 ============
// 必须排在 auth 之后使用(依赖 req.user
function requireAdmin(req, res, next) {
if (!req.user || req.user.role !== 'admin') {
return res.status(403).json({ code: 403, message: '需要管理员权限' })
}
next()
}
// ============ POST /api/login ============
app.post('/api/user/login', async (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 id, username, password, nickname, avatar, role FROM users WHERE username = ?',
[username]
)
// 用户不存在 或 密码错 —— 都用同一条提示,避免被枚举账号
if (rows.length === 0) {
return res.status(400).json({ code: 400, message: '账号或密码错误' })
}
const user = rows[0]
const ok = await bcrypt.compare(password, user.password)
if (!ok) {
return res.status(400).json({ code: 400, message: '账号或密码错误' })
}
// 签 tokenpayload 里放 id / username / role
const token = jwt.sign(
{ id: user.id,
username: user.username,
role: user.role },
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,
nickname: user.nickname,
avatar: user.avatar,
role: user.role,
},
},
})
} catch (e) {
console.error('[login] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
})
// ============ GET /api/info ============
// 需要登录后访问,前端刷新页面时调用,用来拿最新的用户信息
app.get('/api/user/info', auth, async (req, res) => {
try {
const [rows] = await pool.query(
'SELECT id, username, nickname, avatar, role FROM users WHERE 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 })
}
})
// ============ POST /api/logout ============
// JWT 是无状态的:后端没法"作废"token主流做法是前端清掉 localStorage 里的 token
// 这里只是给前端一个明确的应答,告诉你"我收到了"
app.post('/api/user/logout', auth, (req, res) => {
res.json({ code: 0, message: 'ok' })
})
// ============ POST /api/user/create ============
// 仅管理员可调用:在 users 表里新增一行
app.post('/api/user/create', auth, requireAdmin, async (req, res) => {
//role不填默认user
const { username, password, nickname = '', role = 'user' } = req.body || {}
// 1) 入参校验
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 位' })
}
try {
// 2) 密码哈希10 轮 salt与 db.js initDB 的 admin 同强度)
const hash = await bcrypt.hash(password, 10)
// 3) 写入数据库role 不强制 'user',允许前端传来的 role
const [result] = await pool.query(
'INSERT INTO users (username, password, nickname, role) VALUES (?, ?, ?, ?)',
[username, hash, nickname, role]
)
// 4) 把新行的关键信息回给前端(不包含 password 哈希)
res.json({
code: 0,
message: 'ok',
data: { id: result.insertId, username, nickname, role: role },
})
} catch (e) {
// users.username 是 UNIQUE重复时 MySQL 抛 ER_DUP_ENTRY (errno 1062)
if (e.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ code: 409, message: '用户名已存在' })
}
console.error('[create user] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
})
// ============ 启动 ============
const PORT = Number(process.env.PORT) || 3000
initDB()
.then(() => {
app.listen(PORT, () => {
console.log(`[server] 已启动 → http://127.0.0.1:${PORT}`)
console.log(`[test] curl -X POST http://127.0.0.1:${PORT}/api/login \\`)
console.log(` -H "Content-Type: application/json" \\`)
console.log(` -d '{"username":"admin","password":"123456"}'`)
})
})
.catch((err) => {
console.error('[init] 数据库初始化失败:', err)
process.exit(1)
})