- 在后端login控制器中添加详细的执行步骤日志 - 添加错误捕获和处理,防止未捕获的异常 - 在前端LoginForm中添加详细的错误日志输出 - 在rate limiter中添加详细的限制日志 - 在请求日志中添加POST请求体信息 - 帮助定位401错误的具体原因 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
405 lines
11 KiB
TypeScript
405 lines
11 KiB
TypeScript
import { Request, Response } from 'express';
|
||
import bcrypt from 'bcryptjs';
|
||
import jwt from 'jsonwebtoken';
|
||
import { prisma } from '../config/database';
|
||
import { logger } from '../utils/logger';
|
||
import { AuthRequest } from '../middleware/authMiddleware';
|
||
import type { Secret, SignOptions } from 'jsonwebtoken';
|
||
|
||
// Generate JWT token
|
||
function generateToken(userId: string): string {
|
||
const secret = "pandora";
|
||
if (!secret) {
|
||
throw new Error('JWT_SECRET is not configured');
|
||
}
|
||
const expiresIn = process.env.JWT_EXPIRES_IN || '7d';
|
||
return jwt.sign(
|
||
{ userId },
|
||
secret,
|
||
{ expiresIn: expiresIn as any }
|
||
);
|
||
}
|
||
|
||
// Create session
|
||
async function createSession(userId: string, token: string, req: Request) {
|
||
console.log('创建session:', { userId, token: token.substring(0, 20) + '...' })
|
||
|
||
const session = await prisma.session.create({
|
||
data: {
|
||
userId,
|
||
token,
|
||
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
|
||
userAgent: req.get('User-Agent') ?? null,
|
||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
||
},
|
||
});
|
||
|
||
console.log('Session创建成功:', {
|
||
sessionId: session.id,
|
||
expiresAt: session.expiresAt,
|
||
currentTime: new Date()
|
||
})
|
||
|
||
return session;
|
||
}
|
||
|
||
export const authController = {
|
||
// 用户注册
|
||
async register(req: Request, res: Response) {
|
||
const { username, password, confirmPassword } = req.body;
|
||
|
||
// 验证密码确认
|
||
if (password !== confirmPassword) {
|
||
return res.status(400).json({
|
||
error: '密码和确认密码不匹配'
|
||
});
|
||
}
|
||
|
||
// Check if user already exists
|
||
const existingUser = await prisma.user.findUnique({
|
||
where: { username }
|
||
});
|
||
|
||
if (existingUser) {
|
||
return res.status(400).json({
|
||
error: '用户名已存在'
|
||
});
|
||
}
|
||
|
||
// Hash password
|
||
const hashedPassword = await bcrypt.hash(password, parseInt(process.env.BCRYPT_ROUNDS || '12'));
|
||
|
||
// Create user with isActive set to false by default
|
||
const user = await prisma.user.create({
|
||
data: {
|
||
username,
|
||
password: hashedPassword,
|
||
isActive: false, // 新注册用户默认为禁用状态
|
||
},
|
||
});
|
||
|
||
// Create audit log
|
||
await prisma.auditLog.create({
|
||
data: {
|
||
userId: user.id,
|
||
action: 'USER_REGISTERED',
|
||
resource: 'user',
|
||
resourceId: user.id,
|
||
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
|
||
userAgent: req.get('User-Agent') ?? null,
|
||
}
|
||
});
|
||
|
||
return res.status(201).json({
|
||
message: '注册成功,请等待管理员激活您的账户',
|
||
user: {
|
||
id: user.id,
|
||
username: user.username,
|
||
isAdmin: user.isAdmin,
|
||
isActive: user.isActive,
|
||
}
|
||
});
|
||
},
|
||
|
||
// 用户登录
|
||
async login(req: Request, res: Response) {
|
||
try {
|
||
const { username, password } = req.body;
|
||
|
||
console.log('登录请求:', { username, timestamp: new Date().toISOString() });
|
||
|
||
// Find user
|
||
const user = await prisma.user.findUnique({
|
||
where: { username }
|
||
});
|
||
|
||
console.log('查找用户结果:', { found: !!user, isActive: user?.isActive, userId: user?.id });
|
||
|
||
if (!user) {
|
||
// 记录登录失败审计日志
|
||
await prisma.auditLog.create({
|
||
data: {
|
||
userId: null,
|
||
action: 'USER_LOGIN_FAILED',
|
||
resource: 'user',
|
||
resourceId: null,
|
||
details: JSON.stringify({ username, reason: '用户不存在' }),
|
||
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
|
||
userAgent: req.get('User-Agent') ?? null,
|
||
}
|
||
});
|
||
return res.status(401).json({ error: '用户不存在' });
|
||
}
|
||
|
||
// Check if user is active
|
||
if (!user.isActive) {
|
||
// 记录登录失败审计日志
|
||
await prisma.auditLog.create({
|
||
data: {
|
||
userId: user.id,
|
||
action: 'USER_LOGIN_FAILED',
|
||
resource: 'user',
|
||
resourceId: user.id,
|
||
details: JSON.stringify({ username, reason: '账户已被禁用' }),
|
||
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
|
||
userAgent: req.get('User-Agent') ?? null,
|
||
}
|
||
});
|
||
return res.status(401).json({ error: '账户已被禁用,请联系管理员激活' });
|
||
}
|
||
|
||
// Verify password
|
||
console.log('开始验证密码...');
|
||
const isValidPassword = await bcrypt.compare(password, user.password);
|
||
console.log('密码验证结果:', { isValid: isValidPassword });
|
||
|
||
if (!isValidPassword) {
|
||
// 增加登录失败次数
|
||
const loginAttempts = (user.loginAttempts || 0) + 1;
|
||
const updateData: any = { loginAttempts };
|
||
let userDisabled = false;
|
||
|
||
// 如果失败次数达到5次,禁用账户
|
||
if (loginAttempts >= 5) {
|
||
updateData.isActive = false;
|
||
userDisabled = true;
|
||
}
|
||
|
||
await prisma.user.update({
|
||
where: { id: user.id },
|
||
data: updateData
|
||
});
|
||
|
||
// 记录登录失败审计日志
|
||
await prisma.auditLog.create({
|
||
data: {
|
||
userId: user.id,
|
||
action: 'USER_LOGIN_FAILED',
|
||
resource: 'user',
|
||
resourceId: user.id,
|
||
details: JSON.stringify({
|
||
username,
|
||
reason: userDisabled ? '密码错误且账户已被禁用' : '密码错误',
|
||
loginAttempts,
|
||
isDisabled: userDisabled
|
||
}),
|
||
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
|
||
userAgent: req.get('User-Agent') ?? null,
|
||
}
|
||
});
|
||
|
||
if (userDisabled) {
|
||
return res.status(401).json({ error: '登录失败次数过多,账户已被禁用,请联系管理员' });
|
||
}
|
||
return res.status(401).json({
|
||
error: '用户名或密码错误',
|
||
remainingAttempts: 5 - loginAttempts
|
||
});
|
||
}
|
||
|
||
// 登录成功,重置登录失败次数和锁定时间
|
||
await prisma.user.update({
|
||
where: { id: user.id },
|
||
data: {
|
||
loginAttempts: 0,
|
||
lockedUntil: null
|
||
}
|
||
});
|
||
|
||
// Generate token
|
||
console.log('生成token...');
|
||
const token = generateToken(user.id);
|
||
console.log('Token生成成功:', token.substring(0, 20) + '...');
|
||
|
||
// Delete existing sessions for this user (optional - for single session per user)
|
||
console.log('删除用户现有sessions...');
|
||
await prisma.session.deleteMany({
|
||
where: { userId: user.id }
|
||
});
|
||
|
||
// Create session
|
||
console.log('创建新session...');
|
||
await createSession(user.id, token, req);
|
||
|
||
// Update last login
|
||
console.log('更新最后登录时间...');
|
||
await prisma.user.update({
|
||
where: { id: user.id },
|
||
data: { lastLoginAt: new Date() }
|
||
});
|
||
|
||
// Create audit log
|
||
await prisma.auditLog.create({
|
||
data: {
|
||
userId: user.id,
|
||
action: 'USER_LOGIN',
|
||
resource: 'user',
|
||
resourceId: user.id,
|
||
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
|
||
userAgent: req.get('User-Agent') ?? null,
|
||
}
|
||
});
|
||
|
||
console.log('登录成功,返回响应...');
|
||
return res.json({
|
||
message: '登录成功',
|
||
token,
|
||
user: {
|
||
id: user.id,
|
||
username: user.username,
|
||
isAdmin: user.isAdmin,
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('登录过程中发生错误:', error);
|
||
return res.status(500).json({ error: '服务器内部错误' });
|
||
}
|
||
},
|
||
|
||
// 用户登出
|
||
async logout(req: Request, res: Response) {
|
||
const token = req.headers.authorization?.substring(7);
|
||
|
||
if (token) {
|
||
// Delete session
|
||
await prisma.session.deleteMany({
|
||
where: { token }
|
||
});
|
||
}
|
||
|
||
// Create audit log (if we have user info from token)
|
||
try {
|
||
if (token) {
|
||
const decoded = jwt.verify(token, "pandora") as any;
|
||
if (decoded && decoded.userId) {
|
||
await prisma.auditLog.create({
|
||
data: {
|
||
userId: decoded.userId,
|
||
action: 'USER_LOGOUT',
|
||
resource: 'user',
|
||
resourceId: decoded.userId,
|
||
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
|
||
userAgent: req.get('User-Agent') ?? null,
|
||
}
|
||
});
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// Token无效,不记录审计日志
|
||
console.log('登出时token无效,跳过审计日志记录');
|
||
}
|
||
|
||
res.json({ message: '登出成功' });
|
||
},
|
||
|
||
// 获取当前用户信息
|
||
async getCurrentUser(req: AuthRequest, res: Response) {
|
||
if (!req.user) {
|
||
return res.status(401).json({ error: '未授权' });
|
||
}
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: req.user.id },
|
||
select: {
|
||
id: true,
|
||
username: true,
|
||
isAdmin: true,
|
||
isActive: true,
|
||
lastLoginAt: true,
|
||
createdAt: true,
|
||
updatedAt: true,
|
||
}
|
||
});
|
||
|
||
if (!user) {
|
||
return res.status(404).json({ error: '用户不存在' });
|
||
}
|
||
|
||
return res.json({
|
||
user: user
|
||
});
|
||
},
|
||
|
||
// 刷新token
|
||
async refreshToken(req: Request, res: Response) {
|
||
const { refreshToken } = req.body;
|
||
|
||
if (!refreshToken) {
|
||
return res.status(400).json({ error: '刷新令牌是必需的' });
|
||
}
|
||
|
||
try {
|
||
const decoded = jwt.verify(refreshToken, "pandora") as any;
|
||
|
||
const session = await prisma.session.findFirst({
|
||
where: {
|
||
token: refreshToken,
|
||
expiresAt: {
|
||
gt: new Date()
|
||
}
|
||
},
|
||
include: {
|
||
user: {
|
||
select: {
|
||
id: true,
|
||
username: true,
|
||
isAdmin: true,
|
||
isActive: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
if (!session || !session.user || !session.user.isActive) {
|
||
return res.status(401).json({ error: '无效的刷新令牌' });
|
||
}
|
||
|
||
// Generate new token
|
||
const newToken = generateToken(session.user.id);
|
||
|
||
// Update session
|
||
await prisma.session.update({
|
||
where: { id: session.id },
|
||
data: {
|
||
token: newToken,
|
||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||
}
|
||
});
|
||
|
||
return res.json({
|
||
token: newToken,
|
||
user: session.user
|
||
});
|
||
} catch (error) {
|
||
return res.status(401).json({ error: '无效的刷新令牌' });
|
||
}
|
||
},
|
||
|
||
// 调试端点 - 检查用户session状态
|
||
async debugSession(req: AuthRequest, res: Response) {
|
||
if (!req.user) {
|
||
return res.status(401).json({ error: '未授权' });
|
||
}
|
||
|
||
try {
|
||
const sessions = await prisma.session.findMany({
|
||
where: { userId: req.user.id },
|
||
orderBy: { createdAt: 'desc' }
|
||
});
|
||
|
||
return res.json({
|
||
userId: req.user.id,
|
||
username: req.user.username,
|
||
sessions: sessions.map((s: any) => ({
|
||
id: s.id,
|
||
token: s.token.substring(0, 20) + '...',
|
||
expiresAt: s.expiresAt,
|
||
createdAt: s.createdAt,
|
||
isExpired: s.expiresAt < new Date()
|
||
}))
|
||
});
|
||
} catch (error) {
|
||
return res.status(500).json({ error: '获取session信息失败' });
|
||
}
|
||
}
|
||
};
|