168 lines
6.0 KiB
JavaScript
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
|