feat: v1.5 安全加固与体验优化
- 后端添加 express-rate-limit 限流(登录 10次/分钟,API 200次/分钟) - H5 演示登录:自动获取 demo token,401 时自动重试 - 分类删除前查询关联记录数,提示用户影响范围 - 分类添加支持 8 色选择器,替代随机颜色 - 首页 overview card 改为日均+收入+笔数,避免重复显示支出 - 请求模块 401 处理优化:H5 自动刷新 token 并重试原请求
This commit is contained in:
28
server/package-lock.json
generated
28
server/package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"mysql2": "^3.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1042,6 +1043,24 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
@@ -1298,6 +1317,15 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"mysql2": "^3.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dotenv/config'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import rateLimit from 'express-rate-limit'
|
||||
import { authMiddleware } from './middleware/auth'
|
||||
import { initDatabase } from './db/init'
|
||||
import pool from './db/connection'
|
||||
@@ -32,13 +33,32 @@ app.use(cors({
|
||||
credentials: true,
|
||||
}))
|
||||
app.use(express.json({ limit: '10kb' }))
|
||||
|
||||
// 限流:登录接口 — 每分钟最多 10 次
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 10,
|
||||
message: { code: 42900, message: '请求过于频繁,请稍后再试' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
})
|
||||
|
||||
// 限流:通用 API — 每分钟最多 200 次
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 200,
|
||||
message: { code: 42900, message: '请求过于频繁,请稍后再试' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
})
|
||||
|
||||
app.use(authMiddleware)
|
||||
|
||||
app.use('/api/auth', authRoutes)
|
||||
app.use('/api/transactions', transactionRoutes)
|
||||
app.use('/api/stats', statsRoutes)
|
||||
app.use('/api/budget', budgetRoutes)
|
||||
app.use('/api/categories', categoryRoutes)
|
||||
app.use('/api/auth', authLimiter, authRoutes)
|
||||
app.use('/api/transactions', apiLimiter, transactionRoutes)
|
||||
app.use('/api/stats', apiLimiter, statsRoutes)
|
||||
app.use('/api/budget', apiLimiter, budgetRoutes)
|
||||
app.use('/api/categories', apiLimiter, categoryRoutes)
|
||||
|
||||
app.get('/api/health', async (_req, res) => {
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,23 @@ export function signToken(userId: number): string {
|
||||
return Buffer.from(`${payload}:${signature}`).toString('base64')
|
||||
}
|
||||
|
||||
// H5 演示登录 — 创建/复用演示用户
|
||||
router.post('/demo-login', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const demoOpenid = 'h5_demo_user'
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO users (openid, session_key) VALUES (?, ?) ON DUPLICATE KEY UPDATE session_key = ?, id = LAST_INSERT_ID(id)',
|
||||
[demoOpenid, 'demo', 'demo']
|
||||
)
|
||||
const userId = (result as any).insertId
|
||||
const token = signToken(userId)
|
||||
res.json({ code: 0, data: { token, userId } })
|
||||
} catch (err: any) {
|
||||
console.error('[Auth] Demo login failed:', err.message)
|
||||
res.status(500).json({ code: 50000, message: '服务器错误' })
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/login', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { code } = req.body
|
||||
|
||||
@@ -38,6 +38,20 @@ router.post('/', async (req: AuthRequest, res: Response) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 查询分类关联的记录数
|
||||
router.get('/:id/transaction-count', async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT COUNT(*) as count FROM transactions WHERE category_id = ? AND user_id = ?',
|
||||
[req.params.id, req.userId]
|
||||
)
|
||||
res.json({ code: 0, data: { count: (rows as any[])[0].count } })
|
||||
} catch (err) {
|
||||
console.error('[Category] count error:', err)
|
||||
res.status(500).json({ code: 50000, message: '服务器错误' })
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:id', async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const [result] = await pool.query(
|
||||
|
||||
@@ -351,7 +351,14 @@ execa@^1.0.0:
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
express@^4.18.2:
|
||||
express-rate-limit@^8.5.2:
|
||||
version "8.5.2"
|
||||
resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz"
|
||||
integrity sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==
|
||||
dependencies:
|
||||
ip-address "^10.2.0"
|
||||
|
||||
express@^4.18.2, "express@>= 4.11":
|
||||
version "4.22.2"
|
||||
resolved "https://registry.npmjs.org/express/-/express-4.22.2.tgz"
|
||||
integrity sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==
|
||||
@@ -538,6 +545,11 @@ interpret@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz"
|
||||
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
|
||||
|
||||
ip-address@^10.2.0:
|
||||
version "10.2.0"
|
||||
resolved "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz"
|
||||
integrity sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz"
|
||||
|
||||
Reference in New Issue
Block a user