5c12f28ac7
P0: 环境变量校验/事务保护/RBAC权限 P1: 花名册分页/密码重置验证码/ErrorBoundary/N+1查询优化 P2: 导出限制/自定义错误类/body大小限制/代码去重 P3: 自定义确认弹窗替换window.confirm
97 lines
3.4 KiB
TypeScript
97 lines
3.4 KiB
TypeScript
import { Router } from 'express'
|
|
import { registerSchema, loginSchema, refreshSchema, resetPasswordSchema, forgotPasswordSchema, verifyCodeSchema } from '../schemas/auth.schema'
|
|
import { register, login, refresh, resetPassword } from '../services/auth.service'
|
|
import { authLimiter, loginLimiter } from '../middleware/rateLimit'
|
|
import prisma from '../lib/prisma'
|
|
import bcrypt from 'bcryptjs'
|
|
|
|
const router = Router()
|
|
|
|
const codeStore = new Map<string, { code: string; expiresAt: number }>()
|
|
|
|
router.post('/register', authLimiter, async (req, res, next) => {
|
|
try {
|
|
const data = registerSchema.parse(req.body)
|
|
const result = await register(data.orgName, data.phone, data.password)
|
|
res.json({ success: true, data: result })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
router.post('/login', loginLimiter, async (req, res, next) => {
|
|
try {
|
|
const data = loginSchema.parse(req.body)
|
|
const result = await login(data.phone, data.password)
|
|
res.json({ success: true, data: result })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
router.post('/refresh', async (req, res, next) => {
|
|
try {
|
|
const data = refreshSchema.parse(req.body)
|
|
const result = await refresh(data.refreshToken)
|
|
res.json({ success: true, data: result })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
// 发送重置验证码
|
|
router.post('/forgot-password/send-code', authLimiter, async (req, res, next) => {
|
|
try {
|
|
const data = forgotPasswordSchema.parse(req.body)
|
|
const user = await prisma.user.findUnique({ where: { phone: data.phone } })
|
|
if (!user) {
|
|
return res.status(400).json({ success: false, error: { code: 'NOT_FOUND', message: '该手机号未注册' } })
|
|
}
|
|
const code = Math.random().toString().slice(2, 8)
|
|
codeStore.set(data.phone, { code, expiresAt: Date.now() + 5 * 60 * 1000 })
|
|
res.json({ success: true, data: { code, message: '验证码已生成(开发阶段直接返回,生产环境将发送短信)' } })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
// 验证码重置密码
|
|
router.post('/forgot-password/verify', authLimiter, async (req, res, next) => {
|
|
try {
|
|
const data = verifyCodeSchema.parse(req.body)
|
|
const stored = codeStore.get(data.phone)
|
|
if (!stored || stored.expiresAt < Date.now()) {
|
|
return res.status(400).json({ success: false, error: { code: 'CODE_EXPIRED', message: '验证码已过期,请重新获取' } })
|
|
}
|
|
if (stored.code !== data.code) {
|
|
return res.status(400).json({ success: false, error: { code: 'CODE_WRONG', message: '验证码错误' } })
|
|
}
|
|
codeStore.delete(data.phone)
|
|
const passwordHash = await bcrypt.hash(data.newPassword, 10)
|
|
await prisma.user.updateMany({
|
|
where: { phone: data.phone },
|
|
data: { passwordHash },
|
|
})
|
|
res.json({ success: true, data: { message: '密码重置成功' } })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
router.post('/reset-password', authLimiter, async (req, res, next) => {
|
|
try {
|
|
const data = resetPasswordSchema.parse(req.body)
|
|
const stored = codeStore.get(data.phone)
|
|
if (!stored || stored.expiresAt < Date.now()) {
|
|
return res.status(400).json({ success: false, error: { code: 'CODE_EXPIRED', message: '验证码已过期,请重新获取' } })
|
|
}
|
|
codeStore.delete(data.phone)
|
|
const result = await resetPassword(data.phone, data.newPassword)
|
|
res.json({ success: true, data: result })
|
|
} catch (err) {
|
|
next(err)
|
|
}
|
|
})
|
|
|
|
export default router
|