46 lines
1.8 KiB
JavaScript
46 lines
1.8 KiB
JavaScript
import { Router } from 'express'
|
|
import crypto from 'node:crypto'
|
|
import { prisma } from '../db.js'
|
|
import { authRequired, signToken } from '../middleware.js'
|
|
|
|
const router = Router()
|
|
|
|
// 校验密码: 接受 scrypt$salt$hash 格式
|
|
function verifyPassword(plain, stored) {
|
|
if (!stored || !stored.startsWith('scrypt$')) return false
|
|
const parts = stored.split('$')
|
|
if (parts.length !== 3) return false
|
|
const [, salt, hash] = parts
|
|
const computed = crypto.scryptSync(plain, salt, 64).toString('hex')
|
|
// 常数时间比较
|
|
const a = Buffer.from(computed, 'hex')
|
|
const b = Buffer.from(hash, 'hex')
|
|
if (a.length !== b.length) return false
|
|
return crypto.timingSafeEqual(a, b)
|
|
}
|
|
|
|
router.post('/login', async (req, res) => {
|
|
const { email, password } = req.body || {}
|
|
if (!email || !password) return res.status(400).json({ error: '邮箱或密码不能为空' })
|
|
const user = await prisma.user.findUnique({ where: { email: String(email).toLowerCase() } })
|
|
if (!user) return res.status(401).json({ error: '邮箱或密码错误' })
|
|
if (!verifyPassword(String(password), user.password)) {
|
|
return res.status(401).json({ error: '邮箱或密码错误' })
|
|
}
|
|
const payload = { sub: user.id, email: user.email, name: user.name }
|
|
const token = signToken(payload)
|
|
res.json({ token, user: { id: user.id, name: user.name, email: user.email, createdAt: user.createdAt } })
|
|
})
|
|
|
|
router.get('/me', authRequired, async (req, res) => {
|
|
const user = await prisma.user.findUnique({ where: { id: req.user.sub } })
|
|
if (!user) return res.status(401).json({ error: '用户不存在' })
|
|
res.json({ user: { id: user.id, name: user.name, email: user.email, createdAt: user.createdAt } })
|
|
})
|
|
|
|
router.post('/logout', (_req, res) => {
|
|
// JWT 无状态, 前端清 token 即可; 这里返回 200 让前端不用特判
|
|
res.json({ ok: true })
|
|
})
|
|
|
|
export default router |