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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 用户认证路由
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 }