From 3ee4aa6ee70e80b41041ccb5ecdbfb1617037e20 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 8 Jul 2026 15:44:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E6=9C=9F=20mock-server=20=E9=89=B4=E6=9D=83=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mock-server/data/menu.js | 62 +++++++++++++++++ mock-server/data/users.js | 14 ++++ mock-server/index.js | 16 +++++ mock-server/middleware/requireAuth.js | 12 ++++ mock-server/routes/auth.js | 95 +++++++++++++++++++++++++++ mock-server/state.js | 7 ++ 6 files changed, 206 insertions(+) create mode 100644 mock-server/data/menu.js create mode 100644 mock-server/data/users.js create mode 100644 mock-server/index.js create mode 100644 mock-server/middleware/requireAuth.js create mode 100644 mock-server/routes/auth.js create mode 100644 mock-server/state.js diff --git a/mock-server/data/menu.js b/mock-server/data/menu.js new file mode 100644 index 0000000..73bf63a --- /dev/null +++ b/mock-server/data/menu.js @@ -0,0 +1,62 @@ +export const menuTree = [ + { + id: 'system', + name: '系统管理', + path: '/system', + icon: 'SettingOutlined', + type: 'menu', + children: [ + { + id: 'sys_user', + name: '用户管理', + path: '/system/user', + component: 'system/user/UserList', + type: 'menu', + buttons: [ + { code: 'user:add', name: '新增' }, + { code: 'user:edit', name: '修改' }, + { code: 'user:delete', name: '删除' }, + { code: 'user:lock', name: '锁定' }, + { code: 'user:unlock', name: '解锁' }, + { code: 'user:resetPwd', name: '密码重置' } + ] + }, + { + id: 'sys_role', + name: '角色管理', + path: '/system/role', + component: 'system/role/RoleList', + type: 'menu', + buttons: [ + { code: 'role:add', name: '新增' }, + { code: 'role:edit', name: '修改' }, + { code: 'role:delete', name: '删除' } + ] + }, + { + id: 'sys_org', + name: '机构管理', + path: '/system/org', + component: 'system/org/OrgList', + type: 'menu', + buttons: [ + { code: 'org:add', name: '新增' }, + { code: 'org:edit', name: '修改' }, + { code: 'org:delete', name: '删除' } + ] + } + ] + } +] + +export function flattenButtons(tree) { + const codes = [] + function walk(nodes) { + for (const n of nodes) { + if (n.buttons) codes.push(...n.buttons.map((b) => b.code)) + if (n.children) walk(n.children) + } + } + walk(tree) + return codes +} diff --git a/mock-server/data/users.js b/mock-server/data/users.js new file mode 100644 index 0000000..bf215a7 --- /dev/null +++ b/mock-server/data/users.js @@ -0,0 +1,14 @@ +export const users = [ + { + id: 'u001', + username: 'admin', + password: 'Admin@123', + realName: '系统管理员', + phone: '13800000000', + orgId: 'org001', + roleIds: ['role_admin'], + status: 'enabled', // enabled | locked + failCount: 0, + createdAt: '2026-01-01 00:00:00' + } +] diff --git a/mock-server/index.js b/mock-server/index.js new file mode 100644 index 0000000..efc263e --- /dev/null +++ b/mock-server/index.js @@ -0,0 +1,16 @@ +import express from 'express' +import cors from 'cors' +import cookieParser from 'cookie-parser' +import authRoutes from './routes/auth.js' + +const app = express() +app.use(cors({ origin: 'http://localhost:5173', credentials: true })) +app.use(express.json({ limit: '10mb' })) +app.use(cookieParser()) + +app.use('/auth', authRoutes) + +const PORT = 8888 +app.listen(PORT, () => { + console.log(`[mock-server] listening on http://localhost:${PORT}`) +}) diff --git a/mock-server/middleware/requireAuth.js b/mock-server/middleware/requireAuth.js new file mode 100644 index 0000000..bc890ef --- /dev/null +++ b/mock-server/middleware/requireAuth.js @@ -0,0 +1,12 @@ +import { accessTokens } from '../state.js' + +export function requireAuth(req, res, next) { + const header = req.headers.authorization || '' + const token = header.startsWith('Bearer ') ? header.slice(7) : '' + const record = accessTokens.get(token) + if (!record || record.expireAt < Date.now()) { + return res.status(401).json({ code: 40100, msg: '登录已过期', data: null }) + } + req.userId = record.userId + next() +} diff --git a/mock-server/routes/auth.js b/mock-server/routes/auth.js new file mode 100644 index 0000000..71b21ed --- /dev/null +++ b/mock-server/routes/auth.js @@ -0,0 +1,95 @@ +import express from 'express' +import crypto from 'crypto' +import { users } from '../data/users.js' +import { menuTree, flattenButtons } from '../data/menu.js' +import { + accessTokens, + refreshTokens, + ACCESS_TOKEN_TTL_MS, + REFRESH_TOKEN_TTL_MS, + FIXED_SMS_CODE +} from '../state.js' +import { requireAuth } from '../middleware/requireAuth.js' + +const router = express.Router() + +function findUser(username) { + return users.find((u) => u.username === username) +} + +router.post('/sms/send', (req, res) => { + const { username, password } = req.body || {} + const user = findUser(username) + if (!user) return res.json({ code: 40001, msg: '账号或密码错误', data: null }) + if (user.status === 'locked') return res.json({ code: 40002, msg: '账号已锁定,请联系管理员', data: null }) + if (user.password !== password) return res.json({ code: 40001, msg: '账号或密码错误', data: null }) + console.log(`[mock-server] 验证码已发送至 ${user.phone}: ${FIXED_SMS_CODE}`) + res.json({ code: 200, msg: 'ok', data: null }) +}) + +router.post('/login', (req, res) => { + const { username, password, code } = req.body || {} + const user = findUser(username) + if (!user) return res.json({ code: 40001, msg: '账号或密码错误', data: null }) + if (user.status === 'locked') return res.json({ code: 40002, msg: '账号已锁定,请联系管理员', data: null }) + if (user.password !== password) { + user.failCount += 1 + if (user.failCount >= 5) { + user.status = 'locked' + return res.json({ code: 40002, msg: '连续输错5次,账号已锁定', data: null }) + } + return res.json({ code: 40001, msg: '账号或密码错误', data: null }) + } + if (code !== FIXED_SMS_CODE) return res.json({ code: 40003, msg: '验证码错误或已过期', data: null }) + user.failCount = 0 + + const accessToken = crypto.randomUUID() + const refreshToken = crypto.randomUUID() + accessTokens.set(accessToken, { userId: user.id, expireAt: Date.now() + ACCESS_TOKEN_TTL_MS }) + refreshTokens.set(refreshToken, { userId: user.id, expireAt: Date.now() + REFRESH_TOKEN_TTL_MS }) + + res.cookie('refresh_token', refreshToken, { + httpOnly: true, + maxAge: REFRESH_TOKEN_TTL_MS, + sameSite: 'lax' + }) + res.json({ + code: 200, + msg: 'ok', + data: { + accessToken, + userInfo: { id: user.id, username: user.username, realName: user.realName, orgId: user.orgId, roleIds: user.roleIds }, + menuTree, + buttonCodes: flattenButtons(menuTree) + } + }) +}) + +router.post('/refresh', (req, res) => { + const refreshToken = req.cookies?.refresh_token + const record = refreshToken && refreshTokens.get(refreshToken) + if (!record || record.expireAt < Date.now()) { + return res.status(401).json({ code: 40100, msg: '登录已过期,请重新登录', data: null }) + } + const accessToken = crypto.randomUUID() + accessTokens.set(accessToken, { userId: record.userId, expireAt: Date.now() + ACCESS_TOKEN_TTL_MS }) + res.json({ code: 200, msg: 'ok', data: { accessToken } }) +}) + +router.post('/logout', (req, res) => { + const refreshToken = req.cookies?.refresh_token + if (refreshToken) refreshTokens.delete(refreshToken) + res.clearCookie('refresh_token') + res.json({ code: 200, msg: 'ok', data: null }) +}) + +router.get('/userinfo', requireAuth, (req, res) => { + const user = users.find((u) => u.id === req.userId) + res.json({ code: 200, msg: 'ok', data: { id: user.id, username: user.username, realName: user.realName, orgId: user.orgId, roleIds: user.roleIds } }) +}) + +router.get('/menu', requireAuth, (req, res) => { + res.json({ code: 200, msg: 'ok', data: { menuTree, buttonCodes: flattenButtons(menuTree) } }) +}) + +export default router diff --git a/mock-server/state.js b/mock-server/state.js new file mode 100644 index 0000000..99a8770 --- /dev/null +++ b/mock-server/state.js @@ -0,0 +1,7 @@ +export const accessTokens = new Map() // token -> { userId, expireAt } +export const refreshTokens = new Map() // token -> { userId, expireAt } + +// 开发调试用:验证 401 自动刷新流程时可临时调小,验证完成后必须还原 +export const ACCESS_TOKEN_TTL_MS = 30 * 60 * 1000 +export const REFRESH_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000 +export const FIXED_SMS_CODE = '123456'