修复bug

This commit is contained in:
2025-07-08 16:44:04 +08:00
parent aa2416c5d6
commit 1af79c4111
22 changed files with 400 additions and 1036 deletions

View File

@@ -1,4 +1,4 @@
FROM node:18-alpine FROM node:18-alpine3.18
# 设置工作目录 # 设置工作目录
WORKDIR /app WORKDIR /app
@@ -8,17 +8,21 @@ RUN apk add --no-cache \
python3 \ python3 \
make \ make \
g++ \ g++ \
curl curl \
openssl1.1-compat
# 复制 package.json 和 package-lock.json # 复制 package.json 和 package-lock.json
COPY package*.json ./ COPY package*.json ./
# 安装依赖 # 安装依赖
RUN npm ci --only=production && npm cache clean --force RUN npm install
# 复制源代码 # 复制源代码
COPY . . COPY . .
# 构建项目
RUN npm run build
# 创建非root用户 # 创建非root用户
RUN addgroup -g 1001 -S nodejs RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001 RUN adduser -S nodejs -u 1001
@@ -30,9 +34,10 @@ USER nodejs
# 暴露端口 # 暴露端口
EXPOSE 3001 EXPOSE 3001
RUN npx prisma generate
# 健康检查 # 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3001/health || exit 1 CMD curl -f http://localhost:3001/health || exit 1
# 启动命令 # 启动命令
CMD ["npm", "run", "dev"] CMD [ "sh", "-c", "npm run db:push && npm run db:seed && npm run start" ]

View File

@@ -14,8 +14,6 @@ model User {
id String @id @default(cuid()) id String @id @default(cuid())
username String @unique username String @unique
password String password String
firstName String?
lastName String?
isActive Boolean @default(true) isActive Boolean @default(true)
isAdmin Boolean @default(false) isAdmin Boolean @default(false)
lastLoginAt DateTime? lastLoginAt DateTime?

View File

@@ -9,138 +9,37 @@ async function main() {
// 创建管理员用户 // 创建管理员用户
const adminPassword = await bcrypt.hash('admin123', 12); const adminPassword = await bcrypt.hash('admin123', 12);
const admin = await prisma.user.upsert({ const admin = await prisma.user.upsert({
where: { email: 'admin@pandora.com' }, where: { username: 'admin' },
update: {}, update: {},
create: { create: {
email: 'admin@pandora.com',
username: 'admin', username: 'admin',
password: adminPassword, password: adminPassword,
firstName: '管理员',
lastName: '系统',
isAdmin: true, isAdmin: true,
isActive: true, isActive: true,
emailVerified: true,
}, },
}); });
// 创建测试用户
const userPassword = await bcrypt.hash('user123', 12);
const user = await prisma.user.upsert({
where: { email: 'user@pandora.com' },
update: {},
create: {
email: 'user@pandora.com',
username: 'testuser',
password: userPassword,
firstName: '测试',
lastName: '用户',
isAdmin: false,
isActive: true,
emailVerified: true,
},
});
// 创建网站账号
const accounts = [
{
website: 'claude.ai',
accountName: 'claude_pro_1',
token: 'sk-ant-api03-xxx-claude-pro-1',
maxUsers: 3,
currentUsers: 0,
},
{
website: 'openai.com',
accountName: 'gpt4_plus_1',
token: 'sk-xxx-gpt4-plus-1',
maxUsers: 2,
currentUsers: 0,
},
{
website: 'gemini.google.com',
accountName: 'gemini_pro_1',
token: 'AIzaSyCxxx-gemini-pro-1',
maxUsers: 1,
currentUsers: 0,
},
];
for (const accountData of accounts) {
await prisma.websiteAccount.upsert({
where: {
website_accountName: {
website: accountData.website,
accountName: accountData.accountName,
}
},
update: {},
create: accountData,
});
}
// 为管理员分配所有账号 // 为管理员分配所有账号
const allAccounts = await prisma.websiteAccount.findMany(); // const allAccounts = await prisma.websiteAccount.findMany();
for (const account of allAccounts) { // for (const account of allAccounts) {
await prisma.accountAssignment.upsert({ // await prisma.accountAssignment.upsert({
where: { // where: {
userId_accountId: { // userId_accountId: {
userId: admin.id, // userId: admin.id,
accountId: account.id, // accountId: account.id,
} // }
}, // },
update: {}, // update: {},
create: { // create: {
userId: admin.id, // userId: admin.id,
accountId: account.id, // accountId: account.id,
isActive: true, // isActive: true,
}, // },
}); // });
} // }
// 为用户分配部分账号
const userAccounts = await prisma.websiteAccount.findMany({
where: {
website: {
in: ['claude.ai', 'openai.com']
}
}
});
for (const account of userAccounts) {
await prisma.accountAssignment.upsert({
where: {
userId_accountId: {
userId: user.id,
accountId: account.id,
}
},
update: {},
create: {
userId: user.id,
accountId: account.id,
isActive: true,
},
});
}
// 更新账号当前用户数
for (const account of allAccounts) {
const userCount = await prisma.accountAssignment.count({
where: {
accountId: account.id,
isActive: true,
}
});
await prisma.websiteAccount.update({
where: { id: account.id },
data: { currentUsers: userCount }
});
}
console.log('数据库初始化完成!'); console.log('数据库初始化完成!');
console.log('管理员账户:', admin.email, '密码: admin123'); console.log('管理员账户:', admin.username, '密码: admin123');
console.log('测试用户账户:', user.email, '密码: user123');
} }
main() main()

View File

@@ -51,8 +51,6 @@ export const accountController = {
select: { select: {
id: true, id: true,
username: true, username: true,
firstName: true,
lastName: true,
} }
} }
} }

View File

@@ -46,7 +46,7 @@ async function createSession(userId: string, token: string, req: Request) {
export const authController = { export const authController = {
// 用户注册 // 用户注册
async register(req: Request, res: Response) { async register(req: Request, res: Response) {
const { username, password, confirmPassword, firstName, lastName } = req.body; const { username, password, confirmPassword } = req.body;
// 验证密码确认 // 验证密码确认
if (password !== confirmPassword) { if (password !== confirmPassword) {
@@ -74,8 +74,6 @@ export const authController = {
data: { data: {
username, username,
password: hashedPassword, password: hashedPassword,
firstName,
lastName,
isActive: false, // 新注册用户默认为禁用状态 isActive: false, // 新注册用户默认为禁用状态
}, },
}); });
@@ -97,8 +95,6 @@ export const authController = {
user: { user: {
id: user.id, id: user.id,
username: user.username, username: user.username,
firstName: user.firstName,
lastName: user.lastName,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
isActive: user.isActive, isActive: user.isActive,
} }
@@ -237,8 +233,6 @@ export const authController = {
user: { user: {
id: user.id, id: user.id,
username: user.username, username: user.username,
firstName: user.firstName,
lastName: user.lastName,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
} }
}); });
@@ -283,8 +277,6 @@ export const authController = {
select: { select: {
id: true, id: true,
username: true, username: true,
firstName: true,
lastName: true,
isAdmin: true, isAdmin: true,
isActive: true, isActive: true,
lastLoginAt: true, lastLoginAt: true,

View File

@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
import { prisma } from '../config/database'; import { prisma } from '../config/database';
import { AuthRequest } from '../middleware/auth'; import { AuthRequest } from '../middleware/auth';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import type { PrismaClient } from '@prisma/client';
export const userController = { export const userController = {
// 获取所有用户 (管理员) // 获取所有用户 (管理员)
@@ -18,8 +19,6 @@ export const userController = {
if (search) { if (search) {
where.OR = [ where.OR = [
{ username: { contains: search as string, mode: 'insensitive' } }, { username: { contains: search as string, mode: 'insensitive' } },
{ firstName: { contains: search as string, mode: 'insensitive' } },
{ lastName: { contains: search as string, mode: 'insensitive' } }
]; ];
} }
@@ -40,11 +39,8 @@ export const userController = {
select: { select: {
id: true, id: true,
username: true, username: true,
firstName: true,
lastName: true,
isAdmin: true, isAdmin: true,
isActive: true, isActive: true,
totpEnabled: true,
lastLoginAt: true, lastLoginAt: true,
createdAt: true, createdAt: true,
accountAssignments: { accountAssignments: {
@@ -65,12 +61,9 @@ export const userController = {
users: users.map((user: any) => ({ users: users.map((user: any) => ({
id: user.id, id: user.id,
username: user.username, username: user.username,
firstName: user.firstName,
lastName: user.lastName,
role: user.isAdmin ? 'admin' : 'user', role: user.isAdmin ? 'admin' : 'user',
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
isActive: user.isActive, isActive: user.isActive,
totpEnabled: user.totpEnabled,
lastLoginAt: user.lastLoginAt, lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt, createdAt: user.createdAt,
accounts: user.accountAssignments.map((assignment: any) => assignment.accountId) accounts: user.accountAssignments.map((assignment: any) => assignment.accountId)
@@ -102,11 +95,8 @@ export const userController = {
select: { select: {
id: true, id: true,
username: true, username: true,
firstName: true,
lastName: true,
isAdmin: true, isAdmin: true,
isActive: true, isActive: true,
totpEnabled: true,
lastLoginAt: true, lastLoginAt: true,
createdAt: true, createdAt: true,
} }
@@ -120,11 +110,8 @@ export const userController = {
user: { user: {
id: user.id, id: user.id,
username: user.username, username: user.username,
firstName: user.firstName,
lastName: user.lastName,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
isActive: user.isActive, isActive: user.isActive,
totpEnabled: user.totpEnabled,
lastLoginAt: user.lastLoginAt, lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt createdAt: user.createdAt
} }
@@ -134,9 +121,7 @@ export const userController = {
// 更新用户信息 // 更新用户信息
async updateUser(req: AuthRequest, res: Response) { async updateUser(req: AuthRequest, res: Response) {
const { id } = req.params; const { id } = req.params;
const { username, role, firstName, lastName, isActive, loginAttempts } = req.body; const { username, role, isActive, loginAttempts } = req.body;
console.log('收到更新请求:', req.body);
if (!id) { if (!id) {
return res.status(400).json({ error: '用户ID是必需的' }); return res.status(400).json({ error: '用户ID是必需的' });
@@ -157,9 +142,6 @@ export const userController = {
if (typeof isActive === 'boolean') updateData.isActive = isActive; if (typeof isActive === 'boolean') updateData.isActive = isActive;
} }
// 普通用户可以修改这些字段
if (firstName !== undefined) updateData.firstName = firstName;
if (lastName !== undefined) updateData.lastName = lastName;
// 新增:处理密码修改 // 新增:处理密码修改
if (req.body.password && typeof req.body.password === 'string' && req.body.password.trim() !== '') { if (req.body.password && typeof req.body.password === 'string' && req.body.password.trim() !== '') {
@@ -191,11 +173,8 @@ export const userController = {
select: { select: {
id: true, id: true,
username: true, username: true,
firstName: true,
lastName: true,
isAdmin: true, isAdmin: true,
isActive: true, isActive: true,
totpEnabled: true,
lastLoginAt: true, lastLoginAt: true,
createdAt: true, createdAt: true,
password: true, password: true,
@@ -207,11 +186,8 @@ export const userController = {
user: { user: {
id: user.id, id: user.id,
username: user.username, username: user.username,
firstName: user.firstName,
lastName: user.lastName,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
isActive: user.isActive, isActive: user.isActive,
totpEnabled: user.totpEnabled,
lastLoginAt: user.lastLoginAt, lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt, createdAt: user.createdAt,
password: user.password, password: user.password,
@@ -254,7 +230,7 @@ export const userController = {
} }
// 使用事务来确保数据一致性 // 使用事务来确保数据一致性
await prisma.$transaction(async (tx) => { await prisma.$transaction(async (tx: PrismaClient) => {
// 删除用户现有的所有账号分配 // 删除用户现有的所有账号分配
await tx.accountAssignment.deleteMany({ await tx.accountAssignment.deleteMany({
where: { userId: id } where: { userId: id }
@@ -336,11 +312,8 @@ export const userController = {
select: { select: {
id: true, id: true,
username: true, username: true,
firstName: true,
lastName: true,
isAdmin: true, isAdmin: true,
isActive: true, isActive: true,
totpEnabled: true,
lastLoginAt: true, lastLoginAt: true,
createdAt: true, createdAt: true,
} }
@@ -351,12 +324,9 @@ export const userController = {
user: { user: {
id: user.id, id: user.id,
username: user.username, username: user.username,
firstName: user.firstName,
lastName: user.lastName,
role: user.isAdmin ? 'admin' : 'user', role: user.isAdmin ? 'admin' : 'user',
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
isActive: user.isActive, isActive: user.isActive,
totpEnabled: user.totpEnabled,
lastLoginAt: user.lastLoginAt, lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt createdAt: user.createdAt
} }

View File

@@ -16,8 +16,6 @@ router.post('/register', [
} }
return true; return true;
}), }),
body('firstName').optional().isLength({ max: 50 }),
body('lastName').optional().isLength({ max: 50 }),
validateRequest validateRequest
], authController.register); ], authController.register);
@@ -31,15 +29,6 @@ router.post('/login', [
// Logout // Logout
router.post('/logout', authMiddleware, authController.logout); router.post('/logout', authMiddleware, authController.logout);
// Setup TOTP
router.post('/setup-totp', authMiddleware, authController.setupTOTP);
// Verify TOTP
router.post('/verify-totp', [
body('token').notEmpty(),
validateRequest
], authMiddleware, authController.verifyTOTP);
// Get current user // Get current user
router.get('/me', authMiddleware, authController.getCurrentUser); router.get('/me', authMiddleware, authController.getCurrentUser);

View File

@@ -14,7 +14,6 @@ services:
networks: networks:
- pandora_network - pandora_network
restart: always restart: always
command: npm run start
# 前端应用 # 前端应用
frontend: frontend:
@@ -33,7 +32,6 @@ services:
networks: networks:
- pandora_network - pandora_network
restart: always restart: always
command: npm run start
volumes: volumes:
sqlite_data: sqlite_data:

View File

@@ -11,11 +11,14 @@ RUN apk add --no-cache \
COPY package*.json ./ COPY package*.json ./
# 安装依赖 # 安装依赖
RUN npm ci RUN npm install
# 复制源代码 # 复制源代码
COPY . . COPY . .
# 构建项目
RUN npm run build
# 创建非root用户 # 创建非root用户
RUN addgroup -g 1001 -S nodejs RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001 RUN adduser -S nodejs -u 1001
@@ -30,6 +33,5 @@ EXPOSE 3000
# 健康检查 # 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000 || exit 1 CMD curl -f http://localhost:3000 || exit 1
# 启动命令
# 启动命令 - 支持环境变量 CMD ["npm", "run", "start"]
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

View File

@@ -4,8 +4,9 @@
"description": "Pandora 前端应用", "description": "Pandora 前端应用",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite preview --host 0.0.0.0 --port 3000",
"dev": "vite", "dev": "vite",
"build": "vue-tsc && vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest", "test": "vitest",
"test:ui": "vitest --ui", "test:ui": "vitest --ui",
@@ -13,17 +14,19 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"vue": "^3.3.8",
"vue-router": "^4.2.5",
"pinia": "^2.1.7",
"@vueuse/core": "^10.4.1",
"vee-validate": "^4.10.5",
"vue-toastification": "^2.0.0-rc.5",
"axios": "^1.5.0",
"@headlessui/vue": "^1.7.16", "@headlessui/vue": "^1.7.16",
"@heroicons/vue": "^2.0.18", "@heroicons/vue": "^2.0.18",
"@vue/runtime-core": "^3.3.4",
"@vue/runtime-dom": "^3.3.4",
"@vueuse/core": "^10.4.1",
"axios": "^1.5.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"tailwind-merge": "^1.14.0" "pinia": "^2.1.7",
"tailwind-merge": "^1.14.0",
"vee-validate": "^4.10.5",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vue-toastification": "^2.0.0-rc.5"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.6.3", "@types/node": "^20.6.3",
@@ -38,13 +41,13 @@
"postcss": "^8.4.29", "postcss": "^8.4.29",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "~5.2.0", "typescript": "^5.8.3",
"vite": "^4.4.11", "vite": "^4.4.11",
"vitest": "^0.34.4", "vitest": "^0.34.4",
"vue-tsc": "^1.8.15" "vue-tsc": "^3.0.1"
}, },
"engines": { "engines": {
"node": ">=18.0.0", "node": ">=18.0.0",
"npm": ">=8.0.0" "npm": ">=8.0.0"
} }
} }

View File

@@ -11,11 +11,11 @@ export interface WebsiteConfig {
const getWebsiteUrls = () => { const getWebsiteUrls = () => {
// 检查是否在浏览器环境中 // 检查是否在浏览器环境中
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// 在客户端使用Vite定义的全局变量 // 在客户端使用Vite注入的环境变量
return { return {
claude: (window as any).__CLAUDE_URL__ || 'https://chat.micar9.com:8443', claude: import.meta.env.CLAUDE_TARGET_URL || 'https://chat.micar9.com:8443',
chatgpt: (window as any).__CHATGPT_URL__ || 'https://chat.openai.com', chatgpt: import.meta.env.CHATGPT_TARGET_URL || 'https://chat.openai.com',
grok: (window as any).__GROK_URL__ || 'https://grok-mirror.micar9.com:8443' grok: import.meta.env.GROK_TARGET_URL || 'https://grok-mirror.micar9.com:8443'
} }
} }

View File

@@ -1,9 +1,7 @@
import type { RouteRecordRaw, NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useAdminStore } from '@/stores/admin'
import { adminAuth } from '@/utils/auth' import { adminAuth } from '@/utils/auth'
import { useToast } from 'vue-toastification'
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
@@ -74,17 +72,18 @@ const router = createRouter({
}) })
// 路由守卫 // 路由守卫
router.beforeEach((to: any, from: any, next: any) => { router.beforeEach(async (
to: RouteLocationNormalized,
_from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
// 设置页面标题 // 设置页面标题
document.title = `${to.meta.title} - Pandora` const title = to.meta.title as string
document.title = `${title} - Pandora`
// 获取认证状态
const authStore = useAuthStore()
const adminStore = useAdminStore()
const toast = useToast()
// 检查是否需要用户认证 // 检查是否需要用户认证
if (to.meta.requiresAuth) { if (to.meta.requiresAuth) {
const authStore = useAuthStore()
if (!authStore.isLoggedIn) { if (!authStore.isLoggedIn) {
next('/') next('/')
return return

View File

@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { adminAPI } from '@/utils/api' import { adminAPI } from '@/utils/api'
import { adminAuth } from '@/utils/auth' import { adminAuth } from '@/utils/auth'
import type { User, PaginatedResponse } from '@/types' import type { User } from '@/types'
export const useAdminStore = defineStore('admin', () => { export const useAdminStore = defineStore('admin', () => {
// 状态 // 状态

View File

@@ -138,45 +138,6 @@ export const useAuthStore = defineStore('auth', () => {
} }
} }
// 设置TOTP
const setupTOTP = async () => {
loading.value = true
error.value = null
try {
const response = await authAPI.setupTOTP()
return response
} catch (err: any) {
error.value = err.response?.data?.message || '设置二步验证失败'
throw err
} finally {
loading.value = false
}
}
// 验证TOTP
const verifyTOTP = async (totpToken: string) => {
loading.value = true
error.value = null
try {
const response = await authAPI.verifyTOTP(totpToken)
// 更新用户信息
if (user.value && token.value) {
user.value.totpEnabled = true
userAuth.setLogin(token.value, user.value)
}
return response
} catch (err: any) {
error.value = err.response?.data?.message || '验证失败'
throw err
} finally {
loading.value = false
}
}
// 获取用户信息 // 获取用户信息
const getProfile = async () => { const getProfile = async () => {
loading.value = true loading.value = true
@@ -238,8 +199,6 @@ export const useAuthStore = defineStore('auth', () => {
resendVerification, resendVerification,
forgotPassword, forgotPassword,
resetPassword, resetPassword,
setupTOTP,
verifyTOTP,
getProfile, getProfile,
logout, logout,
clearError clearError

View File

@@ -2,12 +2,13 @@
export interface User { export interface User {
id: string id: string
username: string username: string
email: string
role: string role: string
isActive: boolean isActive: boolean
emailVerified?: boolean emailVerified?: boolean
totpEnabled: boolean
createdAt: string createdAt: string
updatedAt: string updatedAt: string
accounts?: string[]
} }
// 网站账号相关类型 // 网站账号相关类型
@@ -47,15 +48,10 @@ export interface Session {
// 审计日志相关类型 // 审计日志相关类型
export interface AuditLog { export interface AuditLog {
id: string id: string
userId: string
action: string action: string
resource: string description: string
resourceId?: string time: string
details?: any ipAddress: string
ipAddress?: string
userAgent?: string
createdAt: string
user: User
} }
// API响应类型 // API响应类型
@@ -116,4 +112,37 @@ export interface Notification {
message: string message: string
duration?: number duration?: number
createdAt: string createdAt: string
}
export interface Account {
id: string
username: string
description: string
}
export interface SystemSettings {
allowRegistration: boolean
sessionTimeout: number
[key: string]: any
}
export interface AdminMenu {
id: string
name: string
description: string
icon: string
path: string
}
export interface AdminUser {
id: string
username: string
role: string
}
export interface SystemStats {
totalUsers: number
totalAccounts: number
todayVisits: number
alerts: number
} }

View File

@@ -113,17 +113,6 @@ export const authAPI = {
return response.data return response.data
}, },
// 设置TOTP
async setupTOTP() {
const response = await api.post('/auth/setup-totp')
return response.data
},
// 验证TOTP
async verifyTOTP(token: string) {
const response = await api.post('/auth/verify-totp', { token })
return response.data
},
// 获取用户信息 // 获取用户信息
async getProfile() { async getProfile() {
@@ -147,26 +136,7 @@ export const authAPI = {
const response = await api.post('/auth/logout') const response = await api.post('/auth/logout')
return response.data return response.data
}, },
// 获取TOTP二维码
async getTOTPQRCode() {
const response = await api.get('/auth/totp/qr-code')
return response.data
},
// 启用TOTP
async enableTOTP(token: string) {
const response = await api.post('/auth/totp/enable', { token })
return response.data
},
// 禁用TOTP
async disableTOTP(password: string) {
const response = await api.post('/auth/totp/disable', { password })
return response.data
}
} }
// 账号管理API // 账号管理API
export const accountAPI = { export const accountAPI = {
// 获取用户可用账号 // 获取用户可用账号

View File

@@ -301,70 +301,94 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAdminStore } from '@/stores/admin' import { useAdminStore } from '@/stores/admin'
import type { AdminMenu, AdminUser, SystemStats } from '@/types'
import { adminAuth } from '@/utils/auth' import { adminAuth } from '@/utils/auth'
import { useToast } from 'vue-toastification'
const router = useRouter() const router = useRouter()
const adminStore = useAdminStore() const adminStore = useAdminStore()
const toast = useToast()
// 管理菜单 const adminUser = ref<AdminUser | null>(null)
const adminMenus = ref([ const stats = ref<SystemStats>({
{
id: 1,
name: '用户管理',
description: '管理用户账号和权限',
icon: 'UserIcon',
route: '/admin/users'
},
{
id: 2,
name: '账号管理',
description: '管理网站账号和token',
icon: 'KeyIcon',
route: '/admin/accounts'
},
{
id: 3,
name: '权限管理',
description: '配置用户访问权限',
icon: 'ShieldIcon',
route: '/admin/permissions'
},
{
id: 4,
name: '系统监控',
description: '查看系统运行状态',
icon: 'ChartIcon',
route: '/admin/monitor'
}
])
// 统计数据
const stats = ref({
totalUsers: 0, totalUsers: 0,
totalAccounts: 0, totalAccounts: 0,
todayVisits: 0, todayVisits: 0,
alerts: 0 alerts: 0
}) })
// 最近活动 const adminMenus: AdminMenu[] = [
const recentActivities = ref([]) {
id: 'users',
name: '用户管理',
description: '管理系统用户',
icon: 'UserIcon',
path: '/admin/users'
},
{
id: 'accounts',
name: '账号管理',
description: '管理网站账号',
icon: 'KeyIcon',
path: '/admin/accounts'
},
{
id: 'permissions',
name: '权限管理',
description: '配置访问权限',
icon: 'ShieldIcon',
path: '/admin/permissions'
},
{
id: 'monitor',
name: '系统监控',
description: '监控系统状态',
icon: 'ChartIcon',
path: '/admin/monitor'
}
]
// 导航到菜单 // 导航到菜单
const navigateToMenu = (menu: any) => { const navigateToMenu = (menu: AdminMenu) => {
router.push(menu.route) router.push(menu.path)
} }
// 管理员用户信息
const adminUser = ref<any>(null)
// 退出登录 // 退出登录
const handleLogout = () => { const handleLogout = async () => {
adminAuth.logout() try {
router.push('/admin/login') await adminStore.logout()
router.push('/admin/login')
} catch (error) {
console.error('退出登录失败:', error)
}
} }
// 加载管理员信息
const loadAdminInfo = async () => {
try {
// 从adminAuth获取管理员信息
const adminInfo = adminAuth.getAdminInfo()
if (adminInfo) {
adminUser.value = adminInfo
}
} catch (error) {
console.error('加载管理员信息失败:', error)
}
}
// 加载系统统计数据
const loadStats = async () => {
try {
const response = await adminStore.getStats()
if (response && response.stats) {
stats.value = response.stats
}
} catch (error) {
console.error('加载系统统计失败:', error)
}
}
// 最近活动
const recentActivities = ref<any[]>([])
// 格式化时间 // 格式化时间
const formatTime = (time: string) => { const formatTime = (time: string) => {
const date = new Date(time) const date = new Date(time)
@@ -382,40 +406,6 @@ const formatTime = (time: string) => {
} }
} }
// 加载统计数据
const loadStats = async () => {
try {
console.log('开始加载统计数据...')
const response = await adminStore.getStats()
console.log('统计数据响应:', response)
if (response.success && response.data) {
stats.value = response.data
console.log('统计数据加载成功:', stats.value)
} else {
console.error('统计数据格式错误:', response)
toast.error('统计数据格式错误')
// 设置默认值
stats.value = {
totalUsers: 0,
totalAccounts: 0,
todayVisits: 0,
alerts: 0
}
}
} catch (error) {
console.error('加载统计数据失败:', error)
toast.error('加载统计数据失败')
// 设置默认值
stats.value = {
totalUsers: 0,
totalAccounts: 0,
todayVisits: 0,
alerts: 0
}
}
}
// 加载最近活动 // 加载最近活动
const loadRecentActivities = async () => { const loadRecentActivities = async () => {
try { try {
@@ -423,50 +413,22 @@ const loadRecentActivities = async () => {
const response = await adminStore.getRecentActivities() const response = await adminStore.getRecentActivities()
console.log('最近活动响应:', response) console.log('最近活动响应:', response)
if (response.success && response.data) { if (response && response.activities) {
recentActivities.value = response.data.activities || [] recentActivities.value = response.activities
console.log('最近活动加载成功:', recentActivities.value) console.log('最近活动加载成功:', recentActivities.value)
} else { } else {
console.error('最近活动数据格式错误:', response) console.error('最近活动数据格式错误:', response)
toast.error('最近活动数据格式错误')
recentActivities.value = [] recentActivities.value = []
} }
} catch (error) { } catch (error) {
console.error('加载最近活动失败:', error) console.error('加载最近活动失败:', error)
toast.error('加载最近活动失败')
recentActivities.value = [] recentActivities.value = []
} }
} }
// 组件挂载时获取数据
onMounted(async () => { onMounted(async () => {
await loadAdminInfo()
// 检查管理员登录状态 await loadStats()
if (!adminAuth.isLoggedIn()) { await loadRecentActivities()
router.push('/admin/login')
return
}
// 获取管理员用户信息
const adminInfo = adminAuth.getAdminInfo()
if (!adminInfo) {
// 如果没有管理员信息,清除登录状态并跳转到登录页
adminAuth.logout()
router.push('/admin/login')
return
}
adminUser.value = adminInfo
// 加载数据
try {
await Promise.all([
loadStats(),
loadRecentActivities()
])
} catch (error) {
console.error('加载数据失败:', error)
}
}) })
</script> </script>

View File

@@ -164,23 +164,6 @@
<div> <div>
<h4 class="text-md font-medium text-gray-900 dark:text-white mb-4">安全设置</h4> <h4 class="text-md font-medium text-gray-900 dark:text-white mb-4">安全设置</h4>
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">二步验证</p>
<p class="text-sm text-gray-500 dark:text-gray-400">是否启用TOTP二步验证</p>
</div>
<button
@click="toggleSystemSetting('enableTOTP')"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
:class="systemSettings.enableTOTP ? 'bg-primary-600' : 'bg-gray-200 dark:bg-gray-700'"
>
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class="systemSettings.enableTOTP ? 'translate-x-5' : 'translate-x-0'"
/>
</button>
</div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm font-medium text-gray-900 dark:text-white">会话超时</p> <p class="text-sm font-medium text-gray-900 dark:text-white">会话超时</p>
@@ -241,85 +224,62 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useAdminStore } from '@/stores/admin' import { useAdminStore } from '@/stores/admin'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import type { User, Account, SystemSettings } from '@/types'
const adminStore = useAdminStore() const adminStore = useAdminStore()
const toast = useToast() const toast = useToast()
// 搜索和筛选 const users = ref<User[]>([])
const availableAccounts = ref<Account[]>([])
const searchQuery = ref('') const searchQuery = ref('')
const accountFilter = ref('') const accountFilter = ref('')
const showAccountModal = ref(false)
// 用户列表 const selectedUser = ref<User | null>(null)
const users = ref([]) const selectedUserAccounts = ref<string[]>([])
const filteredUsers = computed(() => { const systemSettings = ref<SystemSettings>({
let filtered = users.value allowRegistration: false,
if (searchQuery.value) {
filtered = filtered.filter((user: any) =>
user.username.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}
if (accountFilter.value) {
filtered = filtered.filter((user: any) =>
user.accounts?.includes(accountFilter.value)
)
}
return filtered
})
// 可用账号
const availableAccounts = ref([])
// 加载可用账号
const loadAvailableAccounts = async () => {
try {
const response = await adminStore.getAccounts()
availableAccounts.value = response.accounts || []
} catch (error) {
console.error('加载可用账号失败:', error)
// 如果加载失败,使用默认数据
availableAccounts.value = [
{ id: 'account1', username: '账号1', description: '第一个登录账号' },
{ id: 'account2', username: '账号2', description: '第二个登录账号' },
{ id: 'account3', username: '账号3', description: '第三个登录账号' }
]
}
}
// 系统设置
const systemSettings = ref({
allowRegistration: true,
enableTOTP: false,
sessionTimeout: 24 sessionTimeout: 24
}) })
// 模态框状态 // 过滤用户列表
const showAccountModal = ref(false) const filteredUsers = computed(() => {
const selectedUser = ref(null) return users.value.filter(user => {
const selectedUserAccounts = ref([]) const matchesSearch = !searchQuery.value ||
user.username.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesAccount = !accountFilter.value ||
(user.accounts && user.accounts.includes(accountFilter.value))
return matchesSearch && matchesAccount
})
})
// 搜索处理 // 获取账号名称
const handleSearch = () => { const getAccountName = (accountId: string): string => {
// 这里可以添加实际的搜索逻辑
console.log('搜索:', searchQuery.value, '账号筛选:', accountFilter.value)
}
// 根据账户ID获取账户名称
const getAccountName = (accountId: string) => {
const account = availableAccounts.value.find(acc => acc.id === accountId) const account = availableAccounts.value.find(acc => acc.id === accountId)
return account ? account.username : accountId return account ? account.username : accountId
} }
// 编辑用户账号权限 // 编辑用户账号权限
const editUserAccounts = (user: any) => { const editUserAccounts = (user: User) => {
selectedUser.value = { ...user } selectedUser.value = user
selectedUserAccounts.value = [...(user.accounts || [])] selectedUserAccounts.value = user.accounts || []
showAccountModal.value = true showAccountModal.value = true
} }
// 切换系统设置
const toggleSystemSetting = (setting: keyof SystemSettings) => {
if (typeof systemSettings.value[setting] === 'boolean') {
systemSettings.value[setting] = !systemSettings.value[setting]
}
}
// 搜索处理
const handleSearch = () => {
// 实现搜索逻辑
}
// 切换用户账号权限 // 切换用户账号权限
const toggleUserAccount = (accountId: string) => { const toggleUserAccount = (accountId: string) => {
const index = selectedUserAccounts.value.indexOf(accountId) const index = selectedUserAccounts.value.indexOf(accountId)
@@ -333,22 +293,25 @@ const toggleUserAccount = (accountId: string) => {
// 保存用户账号权限 // 保存用户账号权限
const saveUserAccounts = async () => { const saveUserAccounts = async () => {
try { try {
if (!selectedUser.value?.id) {
toast.error('用户ID不存在')
return
}
// 调用API保存用户账号权限到后端 // 调用API保存用户账号权限到后端
await adminStore.updateUserAccounts(selectedUser.value.id, selectedUserAccounts.value) await adminStore.updateUserAccounts(selectedUser.value.id, selectedUserAccounts.value)
console.log('保存用户账号权限:', selectedUser.value?.id, selectedUserAccounts.value) console.log('保存用户账号权限:', selectedUser.value.id, selectedUserAccounts.value)
// 直接更新本地用户数据 // 直接更新本地用户数据
if (selectedUser.value) { const userIndex = users.value.findIndex((user: any) => user.id === selectedUser.value!.id)
const userIndex = users.value.findIndex((user: any) => user.id === selectedUser.value.id) console.log('找到用户索引:', userIndex, '用户ID:', selectedUser.value.id)
console.log('找到用户索引:', userIndex, '用户ID:', selectedUser.value.id) if (userIndex !== -1) {
if (userIndex !== -1) { // 使用Vue的响应式更新机制
// 使用Vue的响应式更新机制 users.value[userIndex] = {
users.value[userIndex] = { ...users.value[userIndex],
...users.value[userIndex], accounts: [...selectedUserAccounts.value]
accounts: [...selectedUserAccounts.value]
}
console.log('更新后的用户数据:', users.value[userIndex])
} }
console.log('更新后的用户数据:', users.value[userIndex])
} }
toast.success('权限保存成功') toast.success('权限保存成功')
@@ -359,12 +322,6 @@ const saveUserAccounts = async () => {
} }
} }
// 切换系统设置
const toggleSystemSetting = (setting: string) => {
systemSettings.value[setting] = !systemSettings.value[setting]
console.log('切换系统设置:', setting, systemSettings.value[setting])
}
// 加载用户列表 // 加载用户列表
const loadUsers = async () => { const loadUsers = async () => {
try { try {
@@ -380,6 +337,22 @@ const loadUsers = async () => {
} }
} }
// 加载可用账号
const loadAvailableAccounts = async () => {
try {
const response = await adminStore.getAccounts()
availableAccounts.value = response.accounts || []
} catch (error) {
console.error('加载可用账号失败:', error)
// 如果加载失败,使用默认数据
availableAccounts.value = [
{ id: 'account1', username: '账号1', description: '第一个登录账号' },
{ id: 'account2', username: '账号2', description: '第二个登录账号' },
{ id: 'account3', username: '账号3', description: '第三个登录账号' }
]
}
}
// 组件挂载时加载数据 // 组件挂载时加载数据
onMounted(async () => { onMounted(async () => {
await loadUsers() await loadUsers()

View File

@@ -103,9 +103,6 @@
<div class="text-sm font-medium text-gray-900 dark:text-white"> <div class="text-sm font-medium text-gray-900 dark:text-white">
{{ user.username }} {{ user.username }}
</div> </div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ user.firstName || user.lastName ? `${user.firstName || ''} ${user.lastName || ''}`.trim() : '未设置姓名' }}
</div>
</div> </div>
</div> </div>
</td> </td>
@@ -269,7 +266,7 @@ const showCreateModal = ref(false)
const createForm = reactive({ const createForm = reactive({
username: '', username: '',
password: '', password: '',
role: 'user' role: 'user' as string
}) })
// 编辑用户模态框 // 编辑用户模态框
@@ -277,7 +274,7 @@ const showEditModal = ref(false)
const editForm = reactive({ const editForm = reactive({
id: '', id: '',
username: '', username: '',
role: 'user', role: 'user' as string,
isActive: true, isActive: true,
password: '' password: ''
}) })
@@ -324,7 +321,11 @@ const changePage = async (page: number) => {
// 创建用户 // 创建用户
const handleCreateUser = async () => { const handleCreateUser = async () => {
try { try {
await adminStore.createUser(createForm) await adminStore.createUser({
username: createForm.username,
password: createForm.password,
role: createForm.role
})
toast.success('用户创建成功') toast.success('用户创建成功')
showCreateModal.value = false showCreateModal.value = false

View File

@@ -226,7 +226,7 @@ onMounted(async () => {
// 加载用户账号 // 加载用户账号
await loadUserAccounts() await loadUserAccounts()
} catch (error) { } catch (error: any) {
console.error('Dashboard初始化失败:', error) console.error('Dashboard初始化失败:', error)
// 如果是认证错误,重定向到登录页面 // 如果是认证错误,重定向到登录页面

View File

@@ -10,20 +10,12 @@ export default defineConfig({
'@': resolve(__dirname, 'src'), '@': resolve(__dirname, 'src'),
}, },
}, },
define: {
// 定义环境变量,使其在客户端可用
VITE_API_URL: JSON.stringify(process.env.VITE_API_URL || 'http://localhost:3001'),
VITE_APP_NAME: JSON.stringify(process.env.VITE_APP_NAME || 'Pandora'),
VITE_CLAUDE_TARGET_URL: JSON.stringify(process.env.CLAUDE_TARGET_URL || 'https://claude.ai'),
VITE_CHATGPT_TARGET_URL: JSON.stringify(process.env.CHATGPT_TARGET_URL || 'https://chat.openai.com'),
VITE_GROK_TARGET_URL: JSON.stringify(process.env.GROK_TARGET_URL || 'https://grok.x.ai'),
},
server: { server: {
port: 3000, port: 3000,
host: '0.0.0.0', host: '0.0.0.0',
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3001', target: 'http://backend:3001',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },

661
package-lock.json generated

File diff suppressed because it is too large Load Diff