修复bug
This commit is contained in:
@@ -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" ]
|
||||||
@@ -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?
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ export const accountController = {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
firstName: true,
|
|
||||||
lastName: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
// 状态
|
// 状态
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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 = {
|
||||||
// 获取用户可用账号
|
// 获取用户可用账号
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 加载用户账号
|
// 加载用户账号
|
||||||
await loadUserAccounts()
|
await loadUserAccounts()
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Dashboard初始化失败:', error)
|
console.error('Dashboard初始化失败:', error)
|
||||||
|
|
||||||
// 如果是认证错误,重定向到登录页面
|
// 如果是认证错误,重定向到登录页面
|
||||||
|
|||||||
@@ -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
661
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user