init
This commit is contained in:
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
PORT=3000
|
||||||
|
|
||||||
|
DB_HOST=
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=backmanager
|
||||||
|
|
||||||
|
JWT_SECRET=
|
||||||
|
JWT_EXPIRES_IN=2h
|
||||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# env
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# coverage
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
66
db.js
Normal file
66
db.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// db.js —— 只做两件事:导出连接池;启动时建库/建表/插 admin
|
||||||
|
require('dotenv').config() //自动寻找.env文件,并把里面的键值对全部导入进来
|
||||||
|
const mysql = require('mysql2/promise')
|
||||||
|
const bcrypt = require('bcryptjs') //用来给密码做哈希加密
|
||||||
|
|
||||||
|
// ============ 1. 连接池 ============
|
||||||
|
// 业务代码里用 pool.query() / pool.execute(),会自动从池里取/还连接
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: Number(process.env.DB_PORT) || 3306,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
waitForConnections: true, // 池里没连接时排队等
|
||||||
|
connectionLimit: 10, // 最多 10 个连接
|
||||||
|
decimalNumbers: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ============ 2. 启动时初始化 ============
|
||||||
|
// 思路:先用一个"无 database"的连接建库,再回到 pool 建表、插 admin
|
||||||
|
async function initDB() {
|
||||||
|
const { DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME } = process.env
|
||||||
|
|
||||||
|
// 2.1 建库(如果不存在)
|
||||||
|
const conn = await mysql.createConnection({
|
||||||
|
host: DB_HOST,
|
||||||
|
port: Number(DB_PORT) || 3306,
|
||||||
|
user: DB_USER,
|
||||||
|
password: DB_PASSWORD,
|
||||||
|
})
|
||||||
|
await conn.query(
|
||||||
|
`CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` DEFAULT CHARACTER SET utf8mb4`
|
||||||
|
)
|
||||||
|
await conn.end()
|
||||||
|
|
||||||
|
// 2.2 建表
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
nickname VARCHAR(50) DEFAULT '',
|
||||||
|
avatar VARCHAR(255) DEFAULT '',
|
||||||
|
role VARCHAR(20) DEFAULT 'user',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 2.3 插 admin(仅当不存在时)
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
'SELECT id FROM users WHERE username = ?',
|
||||||
|
['admin']
|
||||||
|
)
|
||||||
|
if (rows.length === 0) {
|
||||||
|
const hash = await bcrypt.hash('123456', 10)
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO users (username, password, nickname, role) VALUES (?, ?, ?, ?)',
|
||||||
|
['admin', hash, '超级管理员', 'admin']
|
||||||
|
)
|
||||||
|
console.log('[init] 已创建默认账号 admin / 123456')
|
||||||
|
} else {
|
||||||
|
console.log('[init] admin 已存在,跳过')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { pool, initDB }
|
||||||
1121
package-lock.json
generated
Normal file
1121
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "backmanager-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node --watch server.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://chochox.asia/ChoChoX/backmanager-server.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"mysql2": "^3.22.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
169
server.js
Normal file
169
server.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// 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: '账号或密码错误' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 签 token:payload 里放 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.json({
|
||||||
|
code: 0,
|
||||||
|
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)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user