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