Compare commits

..

10 Commits

Author SHA1 Message Date
21f4ff65d9 fix: 修复登录后立即验证token失败的问题
- 修改Dashboard组件,对于已登录用户跳过重复的token验证
- 在LoginForm中添加详细的认证状态日志
- 优化initAuth方法,避免对已登录用户的重复验证
- 添加认证状态检查,确保登录成功后再跳转
- 解决登录成功后立即调用/auth/me导致401的时序问题

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 10:02:47 +08:00
19a8426163 debug: 添加详细的登录调试日志
- 在后端login控制器中添加详细的执行步骤日志
- 添加错误捕获和处理,防止未捕获的异常
- 在前端LoginForm中添加详细的错误日志输出
- 在rate limiter中添加详细的限制日志
- 在请求日志中添加POST请求体信息
- 帮助定位401错误的具体原因

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 09:57:59 +08:00
f583f787f0 fix: 修复登录后跳转然后返回登录页的问题
- 移除Dashboard组件中重复的token验证调用
- 优化auth store的initAuth方法,增加详细错误处理和日志
- 改进API响应拦截器,减少对/auth/me请求的自动重定向
- 添加路由守卫调试日志,方便定位路由跳转问题
- 解决登录时的竞态条件和重复验证问题

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 09:43:57 +08:00
0edceecfe5 修复登录bug 2025-08-05 22:42:02 +08:00
a3264be5f6 get real ip 2025-07-22 00:14:21 +08:00
622a1c1b00 fix show data error 2025-07-21 23:53:44 +08:00
ae47f90af1 fix data error 2025-07-21 23:49:10 +08:00
3375029b1f jump to dashboard 2025-07-21 22:59:53 +08:00
d39efbb62f fix tx 2025-07-21 22:56:02 +08:00
9bc42496f5 revert 3e4f9ad660
revert fix
2025-07-21 22:50:36 +08:00
12 changed files with 173 additions and 79 deletions

View File

@@ -26,7 +26,7 @@ export const adminController = {
action: 'ADMIN_LOGIN_FAILED',
resource: 'ADMIN',
details: JSON.stringify({ username, reason: '管理员不存在' }),
ipAddress: req.ip ?? null,
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
userAgent: req.get('User-Agent') ?? null
});
@@ -46,7 +46,7 @@ export const adminController = {
action: 'ADMIN_LOGIN_FAILED',
resource: 'ADMIN',
details: JSON.stringify({ username, reason: '密码错误' }),
ipAddress: req.ip ?? null,
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
userAgent: req.get('User-Agent') ?? null
});
@@ -74,7 +74,7 @@ export const adminController = {
action: 'ADMIN_LOGIN',
resource: 'ADMIN',
details: { username: admin.username },
ipAddress: req.ip ?? null,
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
userAgent: req.get('User-Agent') ?? null
});
@@ -129,8 +129,6 @@ export const adminController = {
}
});
console.log('统计数据:', { totalUsers, totalAccounts, todayVisits, alerts });
res.json({
success: true,
data: {

View File

@@ -28,7 +28,7 @@ async function createSession(userId: string, token: string, req: Request) {
data: {
userId,
token,
ipAddress: req.ip ?? null,
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
},
@@ -85,7 +85,7 @@ export const authController = {
action: 'USER_REGISTERED',
resource: 'user',
resourceId: user.id,
ipAddress: req.ip ?? null,
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
userAgent: req.get('User-Agent') ?? null,
}
});
@@ -103,13 +103,18 @@ export const authController = {
// 用户登录
async login(req: Request, res: Response) {
const { username, password } = req.body;
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({
@@ -119,7 +124,7 @@ export const authController = {
resource: 'user',
resourceId: null,
details: JSON.stringify({ username, reason: '用户不存在' }),
ipAddress: req.ip ?? null,
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
userAgent: req.get('User-Agent') ?? null,
}
});
@@ -136,7 +141,7 @@ export const authController = {
resource: 'user',
resourceId: user.id,
details: JSON.stringify({ username, reason: '账户已被禁用' }),
ipAddress: req.ip ?? null,
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
userAgent: req.get('User-Agent') ?? null,
}
});
@@ -144,7 +149,10 @@ export const authController = {
}
// Verify password
console.log('开始验证密码...');
const isValidPassword = await bcrypt.compare(password, user.password);
console.log('密码验证结果:', { isValid: isValidPassword });
if (!isValidPassword) {
// 增加登录失败次数
const loginAttempts = (user.loginAttempts || 0) + 1;
@@ -175,7 +183,7 @@ export const authController = {
loginAttempts,
isDisabled: userDisabled
}),
ipAddress: req.ip ?? null,
ipAddress: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || null,
userAgent: req.get('User-Agent') ?? null,
}
});
@@ -199,17 +207,22 @@ export const authController = {
});
// 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() }
@@ -222,11 +235,12 @@ export const authController = {
action: 'USER_LOGIN',
resource: 'user',
resourceId: user.id,
ipAddress: req.ip ?? null,
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,
@@ -236,10 +250,14 @@ export const authController = {
isAdmin: user.isAdmin,
}
});
} catch (error) {
console.error('登录过程中发生错误:', error);
return res.status(500).json({ error: '服务器内部错误' });
}
},
// 用户登出
async logout(req: AuthRequest, res: Response) {
async logout(req: Request, res: Response) {
const token = req.headers.authorization?.substring(7);
if (token) {
@@ -249,18 +267,26 @@ export const authController = {
});
}
// Create audit log
if (req.user) {
await prisma.auditLog.create({
data: {
userId: req.user.id,
action: 'USER_LOGOUT',
resource: 'user',
resourceId: req.user.id,
ipAddress: req.ip ?? null,
userAgent: req.get('User-Agent') ?? null,
// 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: '登出成功' });

View File

@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
import { prisma } from '../config/database';
import { AuthRequest } from '../middleware/auth';
import bcrypt from 'bcryptjs';
import { Prisma } from '@prisma/client';
export const userController = {
// 获取所有用户 (管理员)
@@ -229,7 +230,7 @@ export const userController = {
}
// 使用事务来确保数据一致性
await prisma.$transaction(async (tx) => {
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
// 删除用户现有的所有账号分配
await tx.accountAssignment.deleteMany({
where: { userId: id }

View File

@@ -20,7 +20,7 @@ const app = express();
const PORT = process.env.PORT || 3001;
// 信任代理确保正确获取客户端IP地址
app.set('trust proxy', '154.17.226.99');
app.set('trust proxy', true);
// Security middleware
app.use(helmet());
@@ -37,6 +37,13 @@ const limiter = rateLimit({
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'), // limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests from this IP, please try again later.'
},
// 添加详细日志
handler: (req, res) => {
console.log('Rate limit exceeded for IP:', req.ip, 'Path:', req.path);
res.status(429).json({
error: 'Too many requests from this IP, please try again later.'
});
}
});
app.use('/api/', limiter);
@@ -47,8 +54,14 @@ app.use(express.urlencoded({ extended: true }));
// Request logging
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`, {
ip: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress,
userAgent: req.get('User-Agent'),
body: req.method === 'POST' ? req.body : undefined
});
logger.info(`${req.method} ${req.path}`, {
ip: req.ip,
ip: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress,
userAgent: req.get('User-Agent')
});
next();

View File

@@ -29,7 +29,7 @@ export function errorHandler(
stack: error.stack,
url: req.url,
method: req.method,
ip: req.ip,
ip: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress,
userAgent: req.get('User-Agent')
});

View File

@@ -27,7 +27,7 @@ router.post('/login', [
], authController.login);
// Logout
router.post('/logout', authMiddleware, authController.logout);
router.post('/logout', authController.logout);
// Get current user
router.get('/me', authMiddleware, authController.getCurrentUser);

View File

@@ -99,11 +99,18 @@ const handleLogin = async () => {
}
try {
await authStore.login({
const response = await authStore.login({
username: loginForm.username,
password: loginForm.password
})
console.log('登录成功,响应数据:', response)
console.log('Auth store状态:', {
isLoggedIn: authStore.isLoggedIn,
hasToken: !!authStore.token,
hasUser: !!authStore.user
})
// 如果选择记住我,保存登录信息到本地存储
if (loginForm.rememberMe) {
localStorage.setItem('rememberedUser', JSON.stringify({
@@ -114,9 +121,23 @@ const handleLogin = async () => {
localStorage.removeItem('rememberedUser')
}
router.push('/dashboard')
// 确保状态已正确设置后再跳转
if (authStore.isLoggedIn) {
console.log('认证状态确认成功即将跳转到Dashboard')
router.push('/dashboard')
} else {
console.error('登录后认证状态异常')
toast.error('登录状态异常,请重试')
}
} catch (error: any) {
console.log(error)
console.error('登录失败,详细错误信息:', {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
url: error.config?.url,
method: error.config?.method,
headers: error.config?.headers
})
// 根据不同的错误类型显示不同的错误信息
if (error.response?.status === 401) {
const errorMessage = error.response?.data?.error || '用户名或密码错误'

View File

@@ -1,8 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
import { adminAuth } from '@/utils/auth';
import { createRouter, createWebHistory, type RouteRecordRaw, type NavigationGuardNext, type RouteLocationNormalized } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { adminAuth } from '@/utils/auth'
const routes = [
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
@@ -57,42 +57,53 @@ const routes = [
component: () => import('@/views/NotFound.vue'),
meta: { title: '页面未找到' }
}
];
]
const router = createRouter({
history: createWebHistory(),
routes,
});
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
router.beforeEach(async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
// 设置页面标题
const title = to.meta.title as string;
document.title = `${title} - AI`;
const title = to.meta.title as string
document.title = `${title} - AI`
const authStore = useAuthStore();
const authStore = useAuthStore()
console.log(`路由守卫: ${from.path} -> ${to.path}, 用户登录状态: ${authStore.isLoggedIn}`)
// 检查是否需要用户认证
if (to.meta.requiresAuth) {
if (!authStore.isLoggedIn) {
next('/');
return;
console.log('需要登录,重定向到首页')
next('/')
return
}
}
// 检查是否需要管理员认证
if (to.meta.requiresAdminAuth) {
if (!adminAuth.isLoggedIn()) {
next('/admin/login');
return;
console.log('需要管理员登录,重定向到管理员登录页')
next('/admin/login')
return
}
}
// 用户已登录且访问首页重定向到dashboard
if (to.path === '/' && authStore.isLoggedIn) {
return next('/dashboard');
console.log('用户已登录,重定向到dashboard')
next({ name: 'Dashboard' })
return
}
next();
});
next()
})
export default router;
export default router

View File

@@ -16,18 +16,31 @@ export const useAuthStore = defineStore('auth', () => {
// 初始化认证状态
const initAuth = async () => {
// 如果已经登录,不需要重新初始化
if (isLoggedIn.value) {
console.log('用户已登录,跳过初始化')
return
}
const storedToken = userAuth.getToken()
const storedUser = userAuth.getUserInfo()
if (storedToken && storedUser) {
console.log('从本地存储恢复登录状态')
token.value = storedToken
user.value = storedUser
try {
// 验证token有效性
await getProfile()
} catch (error) {
console.log('Token验证成功用户已登录')
} catch (error: any) {
console.log('Token验证失败清除本地存储:', error.response?.status)
// token失效清除本地存储
logout()
await logout()
throw error // 重新抛出错误,让调用方知道初始化失败
}
} else {
console.log('本地存储中无有效的登录信息')
}
}

View File

@@ -41,12 +41,25 @@ api.interceptors.response.use(
},
(error) => {
if (error.response?.status === 401) {
// 如果是登出请求或获取用户信息的请求,不要自动重定向,让上层处理
if (error.config?.url?.includes('/auth/logout') ||
error.config?.url?.includes('/auth/me')) {
return Promise.reject(error)
}
console.log('收到401响应URL:', error.config?.url)
// Token过期或无效清除所有认证状态
userAuth.logout()
adminAuth.logout()
// 自动重定向到登录页面(如果不是已经在登录页面)
if (window.location.pathname !== '/' && window.location.pathname !== '/login') {
// 只有在非登录相关页面时才自动重定向
const currentPath = window.location.pathname
if (currentPath !== '/' &&
currentPath !== '/login' &&
currentPath !== '/admin/login' &&
!currentPath.includes('/auth')) {
console.log('自动重定向到首页')
window.location.href = '/'
}
}

View File

@@ -6,7 +6,7 @@
<div class="flex justify-between h-16">
<div class="flex items-center">
<h1 class="text-2xl font-bold text-primary-600 dark:text-primary-400">
Pandora 管理后台
AI 管理后台
</h1>
</div>
<div class="flex items-center space-x-4">
@@ -378,8 +378,8 @@ const loadAdminInfo = async () => {
const loadStats = async () => {
try {
const response = await adminStore.getStats()
if (response && response.stats) {
stats.value = response.stats
if (response && response.data) {
stats.value = response.data
}
} catch (error) {
console.error('加载系统统计失败:', error)
@@ -409,12 +409,9 @@ const formatTime = (time: string) => {
// 加载最近活动
const loadRecentActivities = async () => {
try {
console.log('开始加载最近活动...')
const response = await adminStore.getRecentActivities()
console.log('最近活动响应:', response)
if (response && response.activities) {
recentActivities.value = response.activities
if (response && response.data.activities) {
recentActivities.value = response.data.activities
console.log('最近活动加载成功:', recentActivities.value)
} else {
console.error('最近活动数据格式错误:', response)

View File

@@ -201,8 +201,22 @@ const loadUserAccounts = async () => {
// 组件挂载时获取数据
onMounted(async () => {
try {
// 初始化认证状态
authStore.initAuth()
console.log('Dashboard初始化开始当前认证状态:', {
isLoggedIn: authStore.isLoggedIn,
hasToken: !!authStore.token,
hasUser: !!authStore.user,
hasLocalToken: !!localStorage.getItem('userToken'),
hasLocalUser: !!localStorage.getItem('userInfo')
})
// 如果用户已经登录(比如刚从登录页面跳转过来),直接使用当前状态
if (authStore.isLoggedIn) {
console.log('用户已登录,直接加载账号数据')
} else {
// 否则尝试从localStorage恢复状态
console.log('用户未登录,尝试初始化认证状态')
await authStore.initAuth()
}
// 确保用户已登录
if (!authStore.isLoggedIn) {
@@ -211,19 +225,6 @@ onMounted(async () => {
return
}
// 验证token是否有效通过尝试获取用户信息
try {
await authStore.getProfile()
} catch (error: any) {
if (error.response?.status === 401) {
// Token无效清除认证状态并重定向
authStore.logout()
toast.error('登录已过期,请重新登录')
router.push('/')
return
}
}
// 加载用户账号
await loadUserAccounts()
} catch (error: any) {