Compare commits
10 Commits
bfb165a9b8
...
21f4ff65d9
| Author | SHA1 | Date | |
|---|---|---|---|
| 21f4ff65d9 | |||
| 19a8426163 | |||
| f583f787f0 | |||
| 0edceecfe5 | |||
| a3264be5f6 | |||
| 622a1c1b00 | |||
| ae47f90af1 | |||
| 3375029b1f | |||
| d39efbb62f | |||
| 9bc42496f5 |
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
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,19 +267,27 @@ export const authController = {
|
||||
});
|
||||
}
|
||||
|
||||
// Create audit log
|
||||
if (req.user) {
|
||||
// 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: req.user.id,
|
||||
userId: decoded.userId,
|
||||
action: 'USER_LOGOUT',
|
||||
resource: 'user',
|
||||
resourceId: req.user.id,
|
||||
ipAddress: req.ip ?? null,
|
||||
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: '登出成功' });
|
||||
},
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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')
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
// 确保状态已正确设置后再跳转
|
||||
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 || '用户名或密码错误'
|
||||
|
||||
@@ -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
|
||||
@@ -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('本地存储中无有效的登录信息')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = '/'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user