SFT/mock-server/routes/auth.js

168 lines
6.0 KiB
JavaScript

import express from 'express'
import { users, userRoles, rolePermissions, permissions } from '../db/seedAuth.js'
import {
issueTokenPair,
accessTokens,
refreshTokens,
genSmsCode,
verifySmsCode,
loginFailCounts,
MAX_LOGIN_FAIL,
REFRESH_TOKEN_TTL_MS
} from '../state.js'
import { sendOk, sendFail } from '../utils/response.js'
import { requireAuth } from '../middleware/requireAuth.js'
const router = express.Router()
const COOKIE_OPTS = {
httpOnly: true,
sameSite: 'lax',
maxAge: REFRESH_TOKEN_TTL_MS
}
function toLoginVo(user, accessToken, forceChangePwd) {
return {
token: accessToken,
username: user.username,
realName: user.realName,
userId: user.id,
phone: user.phone,
userType: user.userType,
organizationId: user.organizationId,
forceChangePwd: !!forceChangePwd
}
}
function toUserDetailVo(user) {
return {
id: user.id,
username: user.username,
phone: user.phone,
realName: user.realName,
organizationId: user.organizationId,
status: user.status,
email: user.email,
lastLoginTime: user.lastLoginTime,
userType: user.userType
}
}
function issueAndRespond(req, res, user) {
loginFailCounts.set(user.username, 0)
user.lastLoginTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
const { accessToken, refreshToken } = issueTokenPair(user.id)
res.cookie('refresh_token', refreshToken, COOKIE_OPTS)
sendOk(res, toLoginVo(user, accessToken, user.forceChangePwd))
}
// 发送短信验证码:真实生成6位随机码(打印到控制台)+ 固定万能码123456同时有效,5分钟过期
router.post('/send-sms-code', (req, res) => {
const { phone, businessType } = req.body || {}
if (!phone || !businessType) return sendFail(res, '手机号和业务类型不能为空', 400)
genSmsCode(businessType, phone)
sendOk(res, null, '验证码已发送')
})
// 账号密码登录:连续失败5次自动锁定
router.post('/login-password', (req, res) => {
const { username, password } = req.body || {}
if (!username || !password) return sendFail(res, '用户名和密码不能为空', 400)
const user = users.find((u) => u.username === username)
if (!user) return sendFail(res, '用户名或密码错误', 401)
if (user.status === 'LOCKED') return sendFail(res, '账号已锁定,请联系管理员', 403)
if (user.password !== password) {
const fails = (loginFailCounts.get(username) || 0) + 1
loginFailCounts.set(username, fails)
if (fails >= MAX_LOGIN_FAIL) {
user.status = 'LOCKED'
return sendFail(res, '连续输错密码5次,账号已锁定', 403)
}
return sendFail(res, `用户名或密码错误,还可尝试${MAX_LOGIN_FAIL - fails}`, 401)
}
issueAndRespond(req, res, user)
})
// 手机验证码登录
router.post('/login-phone', (req, res) => {
const { phone, smsCode } = req.body || {}
if (!phone || !smsCode) return sendFail(res, '手机号和验证码不能为空', 400)
const user = users.find((u) => u.phone === phone)
if (!user) return sendFail(res, '该手机号未注册', 401)
if (user.status === 'LOCKED') return sendFail(res, '账号已锁定,请联系管理员', 403)
if (!verifySmsCode('LOGIN', phone, smsCode)) return sendFail(res, '验证码错误或已过期', 401)
issueAndRespond(req, res, user)
})
// 刷新访问令牌:从httpOnly Cookie读取refresh_token,无需登录即可访问
router.post('/refresh-token', (req, res) => {
const refreshToken = req.cookies?.refresh_token
const record = refreshToken ? refreshTokens.get(refreshToken) : null
if (!record || record.expireAt < Date.now()) {
return sendFail(res, 'refresh_token已失效,请重新登录', 401, null)
}
const user = users.find((u) => u.id === record.userId)
if (!user) return sendFail(res, '用户不存在', 401)
refreshTokens.delete(refreshToken)
const { accessToken, refreshToken: newRefreshToken } = issueTokenPair(user.id)
res.cookie('refresh_token', newRefreshToken, COOKIE_OPTS)
sendOk(res, toLoginVo(user, accessToken, user.forceChangePwd))
})
// 登出
router.post('/logout', (req, res) => {
const header = req.headers.authorization || ''
const token = header.startsWith('Bearer ') ? header.slice(7) : ''
if (token) accessTokens.delete(token)
const refreshToken = req.cookies?.refresh_token
if (refreshToken) refreshTokens.delete(refreshToken)
res.clearCookie('refresh_token', COOKIE_OPTS)
sendOk(res, null, '已退出登录')
})
// 获取当前用户信息(基于JWT/access_token)
router.post('/user-info', requireAuth, (req, res) => {
const user = users.find((u) => u.id === req.userId)
if (!user) return sendFail(res, '用户不存在', 404)
sendOk(res, toUserDetailVo(user))
})
// 查询用户权限详情(扁平列表)
router.get('/user/permission-detail', requireAuth, (req, res) => {
const userId = Number(req.query.userId)
const myRoleIds = userRoles.filter((ur) => ur.userId === userId).map((ur) => ur.roleId)
const myPermissionIds = new Set(
rolePermissions.filter((rp) => myRoleIds.includes(rp.roleId)).map((rp) => rp.permissionId)
)
const flatList = permissions
.filter((p) => myPermissionIds.has(p.id))
.map((p) => ({
id: p.id,
permissionCode: p.permissionCode,
permissionName: p.permissionName,
permissionType: p.permissionType,
parentId: p.parentId,
level: p.level,
sortOrder: p.sortOrder,
icon: p.icon,
routePath: p.routePath,
pageResourcePathList: p.pageResourcePathList,
apiResourcePathList: p.apiResourcePathList
}))
sendOk(res, flatList)
})
// 修改密码:基于当前登录用户
router.post('/change-password', requireAuth, (req, res) => {
const { oldPassword, newPassword } = req.body || {}
if (!oldPassword || !newPassword) return sendFail(res, '旧密码和新密码不能为空', 400)
const user = users.find((u) => u.id === req.userId)
if (!user) return sendFail(res, '用户不存在', 404)
if (user.password !== oldPassword) return sendFail(res, '旧密码不正确', 400)
user.password = newPassword
user.forceChangePwd = false
sendOk(res, null, '密码修改成功')
})
export default router