You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
6.5 KiB

// 用户认证路由
import express from 'express'
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'
import { v4 as uuidv4 } from 'uuid'
import { db } from '../db/database.js'
const router = express.Router()
// JWT 密钥(生产环境应使用环境变量)
const JWT_SECRET = process.env.JWT_SECRET || 'trip-planner-secret-key-2024'
const JWT_EXPIRES_IN = '7d' // Token 7天过期
// 中间件:验证 JWT Token
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1] // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: '未提供认证令牌' })
}
try {
const user = jwt.verify(token, JWT_SECRET)
req.user = user
next()
} catch (err) {
return res.status(403).json({ error: '无效的认证令牌' })
}
}
// 中间件:可选认证(有 token 则验证,没有则继续)
function optionalAuth(req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (token) {
try {
const user = jwt.verify(token, JWT_SECRET)
req.user = user
} catch (err) {
// Token 无效但继续,视为游客
}
}
next()
}
// 中间件:管理员权限检查
function requireAdmin(req, res, next) {
if (!req.user || req.user.role !== 'admin') {
return res.status(403).json({ error: '需要管理员权限' })
}
next()
}
// 注册新用户
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body
if (!username || !password) {
return res.status(400).json({ error: '用户名和密码不能为空' })
}
if (password.length < 6) {
return res.status(400).json({ error: '密码至少需要6个字符' })
}
// 检查用户名是否已存在
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username)
if (existing) {
return res.status(409).json({ error: '用户名已被使用' })
}
// 检查是否是第一个注册用户(自动成为管理员)
const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get()
const role = userCount.count === 0 ? 'admin' : 'user'
// 哈希密码
const passwordHash = await bcrypt.hash(password, 10)
// 创建用户
const userId = uuidv4()
db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?, ?, ?, ?, ?)').run(
userId, username, email || null, passwordHash, role
)
// 生成 JWT
const token = jwt.sign(
{ id: userId, username, role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
)
// 记录统计
db.prepare('INSERT INTO stats (user_id, event_type, event_data) VALUES (?, ?, ?)').run(
userId, 'user_register', JSON.stringify({ username, role })
)
res.status(201).json({
message: '注册成功',
user: { id: userId, username, role },
token
})
} catch (err) {
console.error('注册失败:', err)
res.status(500).json({ error: '注册失败,请稍后重试' })
}
})
// 用户登录
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body
if (!username || !password) {
return res.status(400).json({ error: '用户名和密码不能为空' })
}
// 查找用户
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username)
if (!user) {
return res.status(401).json({ error: '用户名或密码错误' })
}
// 验证密码
const validPassword = await bcrypt.compare(password, user.password_hash)
if (!validPassword) {
return res.status(401).json({ error: '用户名或密码错误' })
}
// 生成 JWT
const token = jwt.sign(
{ id: user.id, username: user.username, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
)
// 记录统计
db.prepare('INSERT INTO stats (user_id, event_type) VALUES (?, ?)').run(
user.id, 'user_login'
)
res.json({
message: '登录成功',
user: { id: user.id, username: user.username, role: user.role },
token
})
} catch (err) {
console.error('登录失败:', err)
res.status(500).json({ error: '登录失败,请稍后重试' })
}
})
// 获取当前用户信息
router.get('/me', authenticateToken, (req, res) => {
const user = db.prepare('SELECT id, username, email, role, created_at FROM users WHERE id = ?').get(req.user.id)
if (!user) {
return res.status(404).json({ error: '用户不存在' })
}
res.json({ user })
})
// 生成游客 ID
router.post('/guest', (req, res) => {
const guestId = uuidv4()
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() // 24小时后过期
res.json({
guestId,
expiresAt,
message: '游客会话已创建数据将保留24小时'
})
})
// 迁移游客数据到登录用户
router.post('/migrate-guest-data', authenticateToken, (req, res) => {
try {
const { guestId } = req.body
if (!guestId) {
return res.status(400).json({ error: '缺少游客ID' })
}
// 获取游客的所有计划
const guestPlans = db.prepare('SELECT * FROM plans WHERE guest_id = ?').all(guestId)
// 将游客计划转移到用户
let migrated = 0
const stmt = db.prepare('UPDATE plans SET user_id = ?, guest_id = NULL WHERE id = ?')
for (const plan of guestPlans) {
stmt.run(req.user.id, plan.id)
migrated++
}
// 同时迁移游客计划
const guestTempPlans = db.prepare('SELECT * FROM guest_plans WHERE guest_id = ?').all(guestId)
const insertPlan = db.prepare('INSERT INTO plans (id, user_id, name, description, data) VALUES (?, ?, ?, ?, ?)')
for (const gp of guestTempPlans) {
insertPlan.run(uuidv4(), req.user.id, gp.name, gp.description, gp.data)
migrated++
}
// 删除游客临时数据
db.prepare('DELETE FROM guest_plans WHERE guest_id = ?').run(guestId)
// 记录统计
db.prepare('INSERT INTO stats (user_id, event_type, event_data) VALUES (?, ?, ?)').run(
req.user.id, 'guest_data_migrated', JSON.stringify({ guestId, migrated })
)
res.json({ message: '数据迁移成功', migrated })
} catch (err) {
console.error('数据迁移失败:', err)
res.status(500).json({ error: '数据迁移失败' })
}
})
export default router
export { authenticateToken, optionalAuth, requireAdmin }