first commit
This commit is contained in:
38
backend/Dockerfile
Normal file
38
backend/Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apk add --no-cache \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
curl
|
||||
|
||||
# 复制 package.json 和 package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci --only=production && npm cache clean --force
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 创建非root用户
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S nodejs -u 1001
|
||||
|
||||
# 更改文件所有权
|
||||
RUN chown -R nodejs:nodejs /app
|
||||
USER nodejs
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3001
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:3001/health || exit 1
|
||||
|
||||
# 启动命令
|
||||
CMD ["npm", "run", "dev"]
|
||||
347
backend/README.md
Normal file
347
backend/README.md
Normal file
@@ -0,0 +1,347 @@
|
||||
# Pandora 后端 API
|
||||
|
||||
这是 Pandora 项目的后端 API 服务,提供完整的用户认证系统和权限管理功能。
|
||||
|
||||
## 🚀 功能特性
|
||||
|
||||
### 用户认证系统
|
||||
- ✅ 用户注册和登录
|
||||
- ✅ JWT 令牌认证
|
||||
- ✅ 邮箱验证
|
||||
- ✅ 密码重置
|
||||
- ✅ TOTP 二步验证
|
||||
- ✅ 会话管理
|
||||
- ✅ 密码加密 (bcrypt)
|
||||
|
||||
### 权限管理系统
|
||||
- ✅ 基于角色的权限控制
|
||||
- ✅ 路径权限验证
|
||||
- ✅ 用户权限分配
|
||||
- ✅ 权限审计日志
|
||||
|
||||
### 账号管理系统
|
||||
- ✅ 网站账号管理
|
||||
- ✅ 账号分配和撤销
|
||||
- ✅ 账号使用统计
|
||||
- ✅ 多用户共享控制
|
||||
|
||||
### 安全特性
|
||||
- ✅ API 限流
|
||||
- ✅ CORS 配置
|
||||
- ✅ 安全头部 (Helmet)
|
||||
- ✅ 输入验证
|
||||
- ✅ 错误处理
|
||||
- ✅ 审计日志
|
||||
|
||||
## 📋 技术栈
|
||||
|
||||
- **运行时**: Node.js + TypeScript
|
||||
- **框架**: Express.js
|
||||
- **数据库**: PostgreSQL + Prisma ORM
|
||||
- **缓存**: Redis
|
||||
- **认证**: JWT + bcrypt
|
||||
- **二步验证**: TOTP (Google Authenticator)
|
||||
- **邮件**: Nodemailer
|
||||
- **日志**: Winston
|
||||
- **验证**: express-validator
|
||||
|
||||
## 🛠️ 安装和设置
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 环境配置
|
||||
|
||||
复制环境变量文件:
|
||||
|
||||
```bash
|
||||
cp env.example .env
|
||||
```
|
||||
|
||||
编辑 `.env` 文件,配置以下变量:
|
||||
|
||||
```env
|
||||
# 数据库配置
|
||||
DATABASE_URL="postgresql://username:password@localhost:5432/pandora"
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET="your-super-secret-jwt-key-here"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
|
||||
# 邮件配置 (用于邮箱验证和密码重置)
|
||||
SMTP_HOST="smtp.gmail.com"
|
||||
SMTP_PORT=587
|
||||
SMTP_USER="your-email@gmail.com"
|
||||
SMTP_PASS="your-app-password"
|
||||
EMAIL_FROM="noreply@pandora.com"
|
||||
|
||||
# Redis 配置
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
|
||||
# 服务器配置
|
||||
PORT=3001
|
||||
NODE_ENV="development"
|
||||
|
||||
# 安全配置
|
||||
BCRYPT_ROUNDS=12
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
|
||||
# TOTP 配置
|
||||
TOTP_ISSUER="Pandora"
|
||||
TOTP_LABEL="Pandora Authentication"
|
||||
```
|
||||
|
||||
### 3. 数据库设置
|
||||
|
||||
```bash
|
||||
# 生成 Prisma 客户端
|
||||
npm run db:generate
|
||||
|
||||
# 运行数据库迁移
|
||||
npm run db:migrate
|
||||
|
||||
# 初始化测试数据
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
### 4. 启动服务
|
||||
|
||||
```bash
|
||||
# 开发模式
|
||||
npm run dev
|
||||
|
||||
# 生产模式
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
## 📚 API 文档
|
||||
|
||||
### 认证相关 API
|
||||
|
||||
#### 用户注册
|
||||
```http
|
||||
POST /api/auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"username": "username",
|
||||
"password": "password123",
|
||||
"firstName": "张",
|
||||
"lastName": "三"
|
||||
}
|
||||
```
|
||||
|
||||
#### 用户登录
|
||||
```http
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
#### 邮箱验证
|
||||
```http
|
||||
POST /api/auth/verify-email
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"token": "verification-token"
|
||||
}
|
||||
```
|
||||
|
||||
#### 设置 TOTP
|
||||
```http
|
||||
POST /api/auth/setup-totp
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 验证 TOTP
|
||||
```http
|
||||
POST /api/auth/verify-totp
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"token": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取当前用户信息
|
||||
```http
|
||||
GET /api/auth/me
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 用户管理 API
|
||||
|
||||
#### 获取所有用户 (管理员)
|
||||
```http
|
||||
GET /api/users
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
#### 获取用户详情
|
||||
```http
|
||||
GET /api/users/:id
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 更新用户信息
|
||||
```http
|
||||
PUT /api/users/:id
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"firstName": "新名字",
|
||||
"lastName": "新姓氏"
|
||||
}
|
||||
```
|
||||
|
||||
### 账号管理 API
|
||||
|
||||
#### 获取用户账号
|
||||
```http
|
||||
GET /api/accounts/user/assigned
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
#### 创建账号 (管理员)
|
||||
```http
|
||||
POST /api/accounts
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"website": "claude.ai",
|
||||
"accountName": "claude_pro_1",
|
||||
"token": "sk-ant-api03-xxx",
|
||||
"maxUsers": 3
|
||||
}
|
||||
```
|
||||
|
||||
## 🔐 安全说明
|
||||
|
||||
### 密码安全
|
||||
- 使用 bcrypt 进行密码哈希,默认 12 轮加密
|
||||
- 密码最小长度 8 位
|
||||
- 支持密码重置功能
|
||||
|
||||
### 会话管理
|
||||
- JWT 令牌有效期 7 天
|
||||
- 支持令牌刷新
|
||||
- 会话存储在数据库中,支持强制登出
|
||||
|
||||
### 二步验证
|
||||
- 支持 TOTP (Google Authenticator)
|
||||
- 提供 10 个备用码
|
||||
- 可选择性启用
|
||||
|
||||
### API 安全
|
||||
- 所有敏感 API 需要认证
|
||||
- 管理员 API 需要管理员权限
|
||||
- 实现 API 限流防止滥用
|
||||
- 输入验证和清理
|
||||
|
||||
## 🧪 测试账户
|
||||
|
||||
运行 `npm run db:seed` 后会创建以下测试账户:
|
||||
|
||||
### 管理员账户
|
||||
- 邮箱: `admin@pandora.com`
|
||||
- 密码: `admin123`
|
||||
- 权限: 所有账号
|
||||
|
||||
### 测试用户账户
|
||||
- 邮箱: `user@pandora.com`
|
||||
- 密码: `user123`
|
||||
- 权限: claude.ai 和 openai.com 账号
|
||||
|
||||
## 📝 开发说明
|
||||
|
||||
### 项目结构
|
||||
```
|
||||
src/
|
||||
├── config/ # 配置文件
|
||||
│ ├── database.ts # 数据库连接
|
||||
│ └── redis.ts # Redis 连接
|
||||
├── controllers/ # 控制器
|
||||
│ ├── authController.ts
|
||||
│ ├── userController.ts
|
||||
│ └── accountController.ts
|
||||
├── middleware/ # 中间件
|
||||
│ ├── authMiddleware.ts
|
||||
│ ├── adminMiddleware.ts
|
||||
│ ├── validateRequest.ts
|
||||
│ ├── errorHandler.ts
|
||||
│ └── notFoundHandler.ts
|
||||
├── routes/ # 路由
|
||||
│ ├── auth.ts
|
||||
│ ├── users.ts
|
||||
│ └── accounts.ts
|
||||
├── utils/ # 工具函数
|
||||
│ └── logger.ts
|
||||
└── index.ts # 入口文件
|
||||
```
|
||||
|
||||
### 数据库模型
|
||||
- `User`: 用户信息
|
||||
- `WebsiteAccount`: 网站账号
|
||||
- `AccountAssignment`: 账号分配
|
||||
- `Session`: 用户会话
|
||||
- `AuditLog`: 审计日志
|
||||
|
||||
### 开发命令
|
||||
```bash
|
||||
# 开发模式
|
||||
npm run dev
|
||||
|
||||
# 构建
|
||||
npm run build
|
||||
|
||||
# 数据库操作
|
||||
npm run db:generate # 生成 Prisma 客户端
|
||||
npm run db:migrate # 运行迁移
|
||||
npm run db:push # 推送 schema 到数据库
|
||||
npm run db:studio # 打开 Prisma Studio
|
||||
npm run db:seed # 初始化测试数据
|
||||
```
|
||||
|
||||
## 🚨 注意事项
|
||||
|
||||
1. **环境变量**: 确保所有必需的环境变量都已正确配置
|
||||
2. **数据库**: 确保 PostgreSQL 数据库正在运行
|
||||
3. **Redis**: 确保 Redis 服务正在运行
|
||||
4. **邮件服务**: 如果使用邮箱验证功能,需要配置有效的 SMTP 服务
|
||||
5. **JWT 密钥**: 生产环境请使用强随机密钥
|
||||
6. **HTTPS**: 生产环境建议使用 HTTPS
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如有问题,请查看:
|
||||
1. 日志文件 (`logs/` 目录)
|
||||
2. 数据库连接状态
|
||||
3. Redis 连接状态
|
||||
4. 环境变量配置
|
||||
|
||||
## 🔄 更新日志
|
||||
|
||||
### v1.0.0
|
||||
- ✅ 完整的用户认证系统
|
||||
- ✅ JWT 令牌认证
|
||||
- ✅ 邮箱验证功能
|
||||
- ✅ TOTP 二步验证
|
||||
- ✅ 权限管理系统
|
||||
- ✅ 账号管理系统
|
||||
- ✅ 审计日志
|
||||
- ✅ API 限流和安全防护
|
||||
45
backend/package.json
Normal file
45
backend/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "pandora-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Pandora Backend API",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"db:generate": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:studio": "prisma studio",
|
||||
"db:seed": "ts-node prisma/seed.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.7.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemailer": "^6.9.7",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"nodemon": "^3.0.2",
|
||||
"prisma": "^5.7.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
99
backend/prisma/schema.prisma
Normal file
99
backend/prisma/schema.prisma
Normal file
@@ -0,0 +1,99 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
password String
|
||||
firstName String?
|
||||
lastName String?
|
||||
isActive Boolean @default(true)
|
||||
isAdmin Boolean @default(false)
|
||||
lastLoginAt DateTime?
|
||||
loginAttempts Int @default(0)
|
||||
lockedUntil DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
sessions Session[]
|
||||
auditLogs AuditLog[]
|
||||
accountAssignments AccountAssignment[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model WebsiteAccount {
|
||||
id String @id @default(cuid())
|
||||
website String
|
||||
accountName String
|
||||
token String
|
||||
isActive Boolean @default(true)
|
||||
maxUsers Int @default(1)
|
||||
currentUsers Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
accountAssignments AccountAssignment[]
|
||||
|
||||
@@unique([website, accountName])
|
||||
@@map("website_accounts")
|
||||
}
|
||||
|
||||
model AccountAssignment {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
accountId String
|
||||
assignedAt DateTime @default(now())
|
||||
expiresAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
|
||||
// Relations
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
account WebsiteAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, accountId])
|
||||
@@map("account_assignments")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
token String @unique
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("sessions")
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
userId String?
|
||||
action String
|
||||
resource String?
|
||||
resourceId String?
|
||||
details String? // JSON string
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("audit_logs")
|
||||
}
|
||||
153
backend/prisma/seed.ts
Normal file
153
backend/prisma/seed.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('开始初始化数据库...');
|
||||
|
||||
// 创建管理员用户
|
||||
const adminPassword = await bcrypt.hash('admin123', 12);
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: 'admin@pandora.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'admin@pandora.com',
|
||||
username: 'admin',
|
||||
password: adminPassword,
|
||||
firstName: '管理员',
|
||||
lastName: '系统',
|
||||
isAdmin: 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();
|
||||
for (const account of allAccounts) {
|
||||
await prisma.accountAssignment.upsert({
|
||||
where: {
|
||||
userId_accountId: {
|
||||
userId: admin.id,
|
||||
accountId: account.id,
|
||||
}
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId: admin.id,
|
||||
accountId: account.id,
|
||||
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('管理员账户:', admin.email, '密码: admin123');
|
||||
console.log('测试用户账户:', user.email, '密码: user123');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('数据库初始化失败:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
34
backend/src/config/database.ts
Normal file
34
backend/src/config/database.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
declare global {
|
||||
var prisma: PrismaClient | undefined;
|
||||
}
|
||||
|
||||
export const prisma = globalThis.prisma || new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalThis.prisma = prisma;
|
||||
}
|
||||
|
||||
export async function connectDatabase() {
|
||||
try {
|
||||
await prisma.$connect();
|
||||
logger.info('Database connected successfully');
|
||||
} catch (error) {
|
||||
logger.error('Database connection failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnectDatabase() {
|
||||
try {
|
||||
await prisma.$disconnect();
|
||||
logger.info('Database disconnected successfully');
|
||||
} catch (error) {
|
||||
logger.error('Database disconnection failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
545
backend/src/controllers/accountController.ts
Normal file
545
backend/src/controllers/accountController.ts
Normal file
@@ -0,0 +1,545 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma } from '../config/database';
|
||||
import { AuthRequest } from '../middleware/authMiddleware';
|
||||
|
||||
export const accountController = {
|
||||
// 获取所有账号 (管理员)
|
||||
async getAllAccounts(req: Request, res: Response) {
|
||||
const accounts = await prisma.websiteAccount.findMany({
|
||||
include: {
|
||||
accountAssignments: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({
|
||||
accounts: accounts.map((account: any) => ({
|
||||
id: account.id,
|
||||
website: account.website,
|
||||
username: account.accountName,
|
||||
token: account.token,
|
||||
isActive: account.isActive,
|
||||
createdAt: account.createdAt,
|
||||
updatedAt: account.updatedAt,
|
||||
assignedUsers: account.accountAssignments.map((aa: any) => aa.user)
|
||||
}))
|
||||
});
|
||||
},
|
||||
|
||||
// 根据ID获取账号 (管理员)
|
||||
async getAccountById(req: Request, res: Response) {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: '账号ID是必需的' });
|
||||
}
|
||||
|
||||
const account = await prisma.websiteAccount.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
accountAssignments: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: '账号不存在' });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
account: {
|
||||
id: account.id,
|
||||
website: account.website,
|
||||
username: account.accountName,
|
||||
token: account.token,
|
||||
isActive: account.isActive,
|
||||
createdAt: account.createdAt,
|
||||
updatedAt: account.updatedAt,
|
||||
assignedUsers: account.accountAssignments.map((aa: any) => aa.user)
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 创建新账号 (管理员)
|
||||
async createAccount(req: Request, res: Response) {
|
||||
const { website, accountName, token, isActive, maxUsers } = req.body;
|
||||
|
||||
if (!website || !accountName || !token) {
|
||||
return res.status(400).json({ error: '网站、账号名称和token是必需的' });
|
||||
}
|
||||
|
||||
// 检查账号是否已存在
|
||||
const existingAccount = await prisma.websiteAccount.findFirst({
|
||||
where: {
|
||||
website,
|
||||
accountName
|
||||
}
|
||||
});
|
||||
|
||||
if (existingAccount) {
|
||||
return res.status(400).json({ error: '账号已存在' });
|
||||
}
|
||||
|
||||
const account = await prisma.websiteAccount.create({
|
||||
data: {
|
||||
website,
|
||||
accountName,
|
||||
token,
|
||||
isActive: isActive !== undefined ? isActive : true,
|
||||
maxUsers: maxUsers || 1,
|
||||
currentUsers: 0,
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
message: '账号创建成功',
|
||||
account: {
|
||||
id: account.id,
|
||||
website: account.website,
|
||||
username: account.accountName,
|
||||
token: account.token,
|
||||
isActive: account.isActive,
|
||||
createdAt: account.createdAt,
|
||||
updatedAt: account.updatedAt
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 更新账号 (管理员)
|
||||
async updateAccount(req: Request, res: Response) {
|
||||
const { id } = req.params;
|
||||
const { website, accountName, token, maxUsers, isActive } = req.body;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: '账号ID是必需的' });
|
||||
}
|
||||
|
||||
const account = await prisma.websiteAccount.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: '账号不存在' });
|
||||
}
|
||||
|
||||
// 检查是否与其他账号重复(排除当前账号)
|
||||
if (website && accountName) {
|
||||
const existingAccount = await prisma.websiteAccount.findFirst({
|
||||
where: {
|
||||
website,
|
||||
accountName,
|
||||
id: { not: id }
|
||||
}
|
||||
});
|
||||
|
||||
if (existingAccount) {
|
||||
return res.status(400).json({ error: '该网站和账号名称组合已存在' });
|
||||
}
|
||||
}
|
||||
|
||||
const updatedAccount = await prisma.websiteAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
website,
|
||||
accountName,
|
||||
token,
|
||||
maxUsers,
|
||||
isActive,
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({
|
||||
message: '账号更新成功',
|
||||
account: {
|
||||
id: updatedAccount.id,
|
||||
website: updatedAccount.website,
|
||||
username: updatedAccount.accountName,
|
||||
token: updatedAccount.token,
|
||||
isActive: updatedAccount.isActive,
|
||||
createdAt: updatedAccount.createdAt,
|
||||
updatedAt: updatedAccount.updatedAt
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 删除账号 (管理员)
|
||||
async deleteAccount(req: Request, res: Response) {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: '账号ID是必需的' });
|
||||
}
|
||||
|
||||
const account = await prisma.websiteAccount.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: '账号不存在' });
|
||||
}
|
||||
|
||||
// 删除账号相关的所有分配
|
||||
await prisma.$transaction([
|
||||
prisma.accountAssignment.deleteMany({ where: { accountId: id } }),
|
||||
prisma.websiteAccount.delete({ where: { id } })
|
||||
]);
|
||||
|
||||
return res.json({ message: '账号删除成功' });
|
||||
},
|
||||
|
||||
// 获取用户的已分配账号
|
||||
async getUserAccounts(req: AuthRequest, res: Response) {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: '未授权' });
|
||||
}
|
||||
|
||||
const assignments = await prisma.accountAssignment.findMany({
|
||||
where: {
|
||||
userId: req.user.id,
|
||||
isActive: true,
|
||||
OR: [
|
||||
{ expiresAt: null },
|
||||
{ expiresAt: { gt: new Date() } }
|
||||
]
|
||||
},
|
||||
include: {
|
||||
account: {
|
||||
select: {
|
||||
id: true,
|
||||
website: true,
|
||||
accountName: true,
|
||||
token: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({
|
||||
accounts: assignments.map((aa: any) => ({
|
||||
id: aa.account.id,
|
||||
website: aa.account.website,
|
||||
username: aa.account.accountName,
|
||||
token: aa.account.token,
|
||||
isActive: aa.account.isActive,
|
||||
createdAt: aa.account.createdAt,
|
||||
updatedAt: aa.account.updatedAt,
|
||||
assignedAt: aa.assignedAt,
|
||||
expiresAt: aa.expiresAt,
|
||||
}))
|
||||
});
|
||||
},
|
||||
|
||||
// 分配账号给用户 (管理员)
|
||||
async assignAccount(req: Request, res: Response) {
|
||||
const { id } = req.params;
|
||||
const { userId, expiresAt } = req.body;
|
||||
|
||||
if (!id || !userId) {
|
||||
return res.status(400).json({ error: '账号ID和用户ID都是必需的' });
|
||||
}
|
||||
|
||||
// 检查账号是否存在
|
||||
const account = await prisma.websiteAccount.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: '账号不存在' });
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
// 检查账号是否已满
|
||||
if (account.currentUsers >= account.maxUsers) {
|
||||
return res.status(400).json({ error: '账号已达到最大用户数' });
|
||||
}
|
||||
|
||||
// 检查是否已经分配
|
||||
const existingAssignment = await prisma.accountAssignment.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
accountId: id,
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
if (existingAssignment) {
|
||||
return res.status(400).json({ error: '用户已分配此账号' });
|
||||
}
|
||||
|
||||
// 创建分配
|
||||
const assignment = await prisma.accountAssignment.create({
|
||||
data: {
|
||||
userId,
|
||||
accountId: id,
|
||||
expiresAt: expiresAt ? new Date(expiresAt) : null,
|
||||
isActive: true,
|
||||
}
|
||||
});
|
||||
|
||||
// 更新账号当前用户数
|
||||
await prisma.websiteAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
currentUsers: {
|
||||
increment: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({
|
||||
message: '账号分配成功',
|
||||
assignment
|
||||
});
|
||||
},
|
||||
|
||||
// 取消账号分配 (管理员)
|
||||
async unassignAccount(req: Request, res: Response) {
|
||||
const { id, userId } = req.params;
|
||||
|
||||
if (!id || !userId) {
|
||||
return res.status(400).json({ error: '账号ID和用户ID都是必需的' });
|
||||
}
|
||||
|
||||
const assignment = await prisma.accountAssignment.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
accountId: id,
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!assignment) {
|
||||
return res.status(404).json({ error: '分配不存在' });
|
||||
}
|
||||
|
||||
// 删除分配
|
||||
await prisma.accountAssignment.delete({
|
||||
where: { id: assignment.id }
|
||||
});
|
||||
|
||||
// 更新账号当前用户数
|
||||
await prisma.websiteAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
currentUsers: {
|
||||
decrement: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({ message: '账号分配已取消' });
|
||||
},
|
||||
|
||||
// 网站登录
|
||||
async loginToWebsite(req: Request, res: Response) {
|
||||
const { accountId } = req.params;
|
||||
const { userId } = req.body;
|
||||
|
||||
if (!accountId || !userId) {
|
||||
return res.status(400).json({ error: '账号ID和用户ID是必需的' });
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取账号信息
|
||||
const account = await prisma.websiteAccount.findUnique({
|
||||
where: { id: accountId }
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
return res.status(404).json({ error: '账号不存在' });
|
||||
}
|
||||
|
||||
// 检查用户是否有权限访问该账号
|
||||
console.log('检查用户权限:', { accountId, userId });
|
||||
|
||||
const assignment = await prisma.accountAssignment.findFirst({
|
||||
where: {
|
||||
accountId,
|
||||
userId,
|
||||
isActive: true,
|
||||
OR: [
|
||||
{ expiresAt: null },
|
||||
{ expiresAt: { gt: new Date() } }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
console.log('权限检查结果:', {
|
||||
assignment: assignment ? 'found' : 'not found',
|
||||
accountId,
|
||||
userId,
|
||||
currentTime: new Date()
|
||||
});
|
||||
|
||||
if (!assignment) {
|
||||
// 获取更多调试信息
|
||||
const allAssignments = await prisma.accountAssignment.findMany({
|
||||
where: { accountId, userId }
|
||||
});
|
||||
console.log('该用户的所有分配记录:', allAssignments);
|
||||
|
||||
return res.status(403).json({ error: '您没有权限访问该账号' });
|
||||
}
|
||||
|
||||
let loginUrl = '';
|
||||
|
||||
// 根据网站类型处理登录
|
||||
switch (account.website) {
|
||||
case 'claude':
|
||||
loginUrl = await handleClaudeLogin(account.token, userId);
|
||||
break;
|
||||
case 'chatgpt':
|
||||
loginUrl = await handleChatGPTLogin(account.token, userId);
|
||||
break;
|
||||
case 'grok':
|
||||
loginUrl = await handleGrokLogin(account.token, userId);
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({ error: '不支持的网站类型' });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
loginUrl,
|
||||
website: account.website,
|
||||
accountName: account.accountName
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('网站登录失败:', error);
|
||||
return res.status(500).json({ error: '登录失败,请稍后重试' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Claude 登录处理
|
||||
async function handleClaudeLogin(token: string, userName: string): Promise<string> {
|
||||
try {
|
||||
const baseUrl = process.env.CLAUDE_TARGET_URL || 'https://chat.micar9.com:8443';
|
||||
console.log('Claude登录处理:', { token, userName });
|
||||
// 第一步:获取oauth token
|
||||
const oauthResponse = await fetch(`${baseUrl}/manage-api/auth/oauth_token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
session_key: token,
|
||||
unique_name: userName
|
||||
})
|
||||
});
|
||||
|
||||
if (!oauthResponse.ok) {
|
||||
throw new Error(`OAuth token 请求失败: ${oauthResponse.status}`);
|
||||
}
|
||||
|
||||
const oauthData = await oauthResponse.json() as { login_url?: string };
|
||||
|
||||
if (!oauthData.login_url) {
|
||||
throw new Error('未获取到登录URL');
|
||||
}
|
||||
|
||||
return oauthData.login_url;
|
||||
} catch (error) {
|
||||
console.error('Claude登录处理失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ChatGPT 登录处理
|
||||
async function handleChatGPTLogin(token: string, userName: string): Promise<string> {
|
||||
try {
|
||||
const baseUrl = process.env.CLAUDE_TARGET_URL || 'http://127.0.0.1:8181';
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.ADMIN_PASSWORD || 'admin'}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
access_token: token,
|
||||
user_name: userName,
|
||||
isolated_session: true,
|
||||
limits: []
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`ChatGPT登录请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as { login_url?: string };
|
||||
|
||||
if (!data.login_url) {
|
||||
throw new Error('未获取到登录URL');
|
||||
}
|
||||
|
||||
return data.login_url;
|
||||
} catch (error) {
|
||||
console.error('ChatGPT登录处理失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Grok 登录处理
|
||||
async function handleGrokLogin(token: string, userName: string): Promise<string> {
|
||||
try {
|
||||
const baseUrl = process.env.CLAUDE_TARGET_URL || 'https://grok-mirror.micar9.com:8443';
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/login-v2`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_name: userName,
|
||||
sso_token: token
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Grok登录请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as { login_url?: string };
|
||||
|
||||
if (!data.login_url) {
|
||||
throw new Error('未获取到登录URL');
|
||||
}
|
||||
|
||||
return data.login_url;
|
||||
} catch (error) {
|
||||
console.error('Grok登录处理失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
222
backend/src/controllers/adminController.ts
Normal file
222
backend/src/controllers/adminController.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { Request, Response } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { prisma } from '../config/database';
|
||||
import { logger } from '../utils/logger';
|
||||
import { createAuditLog } from '../utils/audit';
|
||||
|
||||
export const adminController = {
|
||||
// 管理员登录
|
||||
async login(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
// 验证管理员凭据
|
||||
const admin = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
isAdmin: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
// 记录管理员登录失败审计日志
|
||||
await createAuditLog({
|
||||
userId: null,
|
||||
action: 'ADMIN_LOGIN_FAILED',
|
||||
resource: 'ADMIN',
|
||||
details: JSON.stringify({ username, reason: '管理员不存在' }),
|
||||
ipAddress: req.ip ?? null,
|
||||
userAgent: req.get('User-Agent') ?? null
|
||||
});
|
||||
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: '管理员凭据无效'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isValidPassword = await bcrypt.compare(password, admin.password);
|
||||
if (!isValidPassword) {
|
||||
// 记录管理员登录失败审计日志
|
||||
await createAuditLog({
|
||||
userId: admin.id,
|
||||
action: 'ADMIN_LOGIN_FAILED',
|
||||
resource: 'ADMIN',
|
||||
details: JSON.stringify({ username, reason: '密码错误' }),
|
||||
ipAddress: req.ip ?? null,
|
||||
userAgent: req.get('User-Agent') ?? null
|
||||
});
|
||||
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: '管理员凭据无效'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: admin.id,
|
||||
username: admin.username,
|
||||
role: admin.isAdmin ? 'admin' : 'user'
|
||||
},
|
||||
process.env.JWT_SECRET!,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
// 创建审计日志
|
||||
await createAuditLog({
|
||||
userId: admin.id,
|
||||
action: 'ADMIN_LOGIN',
|
||||
resource: 'ADMIN',
|
||||
details: { username: admin.username },
|
||||
ipAddress: req.ip ?? null,
|
||||
userAgent: req.get('User-Agent') ?? null
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '管理员登录成功',
|
||||
token,
|
||||
admin: {
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
role: admin.isAdmin ? 'admin' : 'user'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Admin login error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 获取统计数据
|
||||
async getStats(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
// 获取用户总数
|
||||
const totalUsers = await prisma.user.count({
|
||||
where: { isAdmin: false }
|
||||
});
|
||||
|
||||
// 获取账号总数
|
||||
const totalAccounts = await prisma.websiteAccount.count();
|
||||
|
||||
// 获取今日访问数(基于会话)
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const todayVisits = await prisma.session.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: today
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 获取系统告警数(基于审计日志中的错误)
|
||||
const alerts = await prisma.auditLog.count({
|
||||
where: {
|
||||
action: { contains: 'ERROR' },
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 24 * 60 * 60 * 1000) // 最近24小时
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('统计数据:', { totalUsers, totalAccounts, todayVisits, alerts });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalUsers,
|
||||
totalAccounts,
|
||||
todayVisits,
|
||||
alerts
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get stats error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取统计数据失败'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 获取最近活动
|
||||
async getRecentActivities(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const activities = await prisma.auditLog.findMany({
|
||||
take: 10,
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const formattedActivities = activities.map((activity: any) => {
|
||||
let description = '';
|
||||
|
||||
if (activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED') {
|
||||
// 解析失败原因
|
||||
let reason = '登录失败';
|
||||
let username = '未知用户';
|
||||
|
||||
if (activity.details) {
|
||||
try {
|
||||
const details = JSON.parse(activity.details);
|
||||
reason = details.reason || '登录失败';
|
||||
username = details.username || '未知用户';
|
||||
} catch (e) {
|
||||
console.error('解析活动详情失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
description = `${username} ${reason}`;
|
||||
} else if (activity.user) {
|
||||
description = `${activity.user.username} ${activity.action}`;
|
||||
} else {
|
||||
description = `系统 ${activity.action}`;
|
||||
}
|
||||
|
||||
return {
|
||||
id: activity.id,
|
||||
description,
|
||||
time: activity.createdAt,
|
||||
details: activity.details,
|
||||
ipAddress: activity.ipAddress,
|
||||
userAgent: activity.userAgent,
|
||||
action: activity.action
|
||||
};
|
||||
});
|
||||
|
||||
console.log('最近活动:', formattedActivities);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
activities: formattedActivities
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get recent activities error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取最近活动失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
57
backend/src/controllers/auditLogController.ts
Normal file
57
backend/src/controllers/auditLogController.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma } from '../config/database';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export const auditLogController = {
|
||||
// 获取审计日志
|
||||
async getAuditLogs(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { page = 1, limit = 10, userId, action, resource } = req.query;
|
||||
const skip = (Number(page) - 1) * Number(limit);
|
||||
|
||||
const where: any = {};
|
||||
if (userId) where.userId = String(userId);
|
||||
if (action) where.action = { contains: String(action) };
|
||||
if (resource) where.resource = { contains: String(resource) };
|
||||
|
||||
const [logs, total] = await Promise.all([
|
||||
prisma.auditLog.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
prisma.auditLog.count({ where })
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
logs,
|
||||
pagination: {
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
total,
|
||||
totalPages: Math.ceil(total / Number(limit))
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Get audit logs error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取审计日志失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
387
backend/src/controllers/authController.ts
Normal file
387
backend/src/controllers/authController.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import { Request, Response } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { prisma } from '../config/database';
|
||||
import { logger } from '../utils/logger';
|
||||
import { AuthRequest } from '../middleware/authMiddleware';
|
||||
import type { Secret, SignOptions } from 'jsonwebtoken';
|
||||
|
||||
// Generate JWT token
|
||||
function generateToken(userId: string): string {
|
||||
const secret = process.env.JWT_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error('JWT_SECRET is not configured');
|
||||
}
|
||||
const expiresIn = process.env.JWT_EXPIRES_IN || '7d';
|
||||
return jwt.sign(
|
||||
{ userId },
|
||||
secret,
|
||||
{ expiresIn: expiresIn as any }
|
||||
);
|
||||
}
|
||||
|
||||
// Create session
|
||||
async function createSession(userId: string, token: string, req: Request) {
|
||||
console.log('创建session:', { userId, token: token.substring(0, 20) + '...' })
|
||||
|
||||
const session = await prisma.session.create({
|
||||
data: {
|
||||
userId,
|
||||
token,
|
||||
ipAddress: req.ip ?? null,
|
||||
userAgent: req.get('User-Agent') ?? null,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Session创建成功:', {
|
||||
sessionId: session.id,
|
||||
expiresAt: session.expiresAt,
|
||||
currentTime: new Date()
|
||||
})
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export const authController = {
|
||||
// 用户注册
|
||||
async register(req: Request, res: Response) {
|
||||
const { username, password, confirmPassword, firstName, lastName } = req.body;
|
||||
|
||||
// 验证密码确认
|
||||
if (password !== confirmPassword) {
|
||||
return res.status(400).json({
|
||||
error: '密码和确认密码不匹配'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
error: '用户名已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, parseInt(process.env.BCRYPT_ROUNDS || '12'));
|
||||
|
||||
// Create user with isActive set to false by default
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
firstName,
|
||||
lastName,
|
||||
isActive: false, // 新注册用户默认为禁用状态
|
||||
},
|
||||
});
|
||||
|
||||
// Create audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
action: 'USER_REGISTERED',
|
||||
resource: 'user',
|
||||
resourceId: user.id,
|
||||
ipAddress: req.ip ?? null,
|
||||
userAgent: req.get('User-Agent') ?? null,
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
message: '注册成功,请等待管理员激活您的账户',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
isAdmin: user.isAdmin,
|
||||
isActive: user.isActive,
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 用户登录
|
||||
async login(req: Request, res: Response) {
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Find user
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
// 记录登录失败审计日志
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: null,
|
||||
action: 'USER_LOGIN_FAILED',
|
||||
resource: 'user',
|
||||
resourceId: null,
|
||||
details: JSON.stringify({ username, reason: '用户不存在' }),
|
||||
ipAddress: req.ip ?? null,
|
||||
userAgent: req.get('User-Agent') ?? null,
|
||||
}
|
||||
});
|
||||
return res.status(401).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if (!user.isActive) {
|
||||
// 记录登录失败审计日志
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
action: 'USER_LOGIN_FAILED',
|
||||
resource: 'user',
|
||||
resourceId: user.id,
|
||||
details: JSON.stringify({ username, reason: '账户已被禁用' }),
|
||||
ipAddress: req.ip ?? null,
|
||||
userAgent: req.get('User-Agent') ?? null,
|
||||
}
|
||||
});
|
||||
return res.status(401).json({ error: '账户已被禁用,请联系管理员激活' });
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isValidPassword = await bcrypt.compare(password, user.password);
|
||||
if (!isValidPassword) {
|
||||
// 增加登录失败次数
|
||||
const loginAttempts = (user.loginAttempts || 0) + 1;
|
||||
const updateData: any = { loginAttempts };
|
||||
let userDisabled = false;
|
||||
|
||||
// 如果失败次数达到5次,禁用账户
|
||||
if (loginAttempts >= 5) {
|
||||
updateData.isActive = false;
|
||||
userDisabled = true;
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: updateData
|
||||
});
|
||||
|
||||
// 记录登录失败审计日志
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
action: 'USER_LOGIN_FAILED',
|
||||
resource: 'user',
|
||||
resourceId: user.id,
|
||||
details: JSON.stringify({
|
||||
username,
|
||||
reason: userDisabled ? '密码错误且账户已被禁用' : '密码错误',
|
||||
loginAttempts,
|
||||
isDisabled: userDisabled
|
||||
}),
|
||||
ipAddress: req.ip ?? null,
|
||||
userAgent: req.get('User-Agent') ?? null,
|
||||
}
|
||||
});
|
||||
|
||||
if (userDisabled) {
|
||||
return res.status(401).json({ error: '登录失败次数过多,账户已被禁用,请联系管理员' });
|
||||
}
|
||||
return res.status(401).json({
|
||||
error: '用户名或密码错误',
|
||||
remainingAttempts: 5 - loginAttempts
|
||||
});
|
||||
}
|
||||
|
||||
// 登录成功,重置登录失败次数和锁定时间
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
loginAttempts: 0,
|
||||
lockedUntil: null
|
||||
}
|
||||
});
|
||||
|
||||
// Generate token
|
||||
const token = generateToken(user.id);
|
||||
|
||||
// Delete existing sessions for this user (optional - for single session per user)
|
||||
await prisma.session.deleteMany({
|
||||
where: { userId: user.id }
|
||||
});
|
||||
|
||||
// Create session
|
||||
await createSession(user.id, token, req);
|
||||
|
||||
// Update last login
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLoginAt: new Date() }
|
||||
});
|
||||
|
||||
// Create audit log
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
action: 'USER_LOGIN',
|
||||
resource: 'user',
|
||||
resourceId: user.id,
|
||||
ipAddress: req.ip ?? null,
|
||||
userAgent: req.get('User-Agent') ?? null,
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({
|
||||
message: '登录成功',
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
isAdmin: user.isAdmin,
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 用户登出
|
||||
async logout(req: AuthRequest, res: Response) {
|
||||
const token = req.headers.authorization?.substring(7);
|
||||
|
||||
if (token) {
|
||||
// Delete session
|
||||
await prisma.session.deleteMany({
|
||||
where: { token }
|
||||
});
|
||||
}
|
||||
|
||||
// Create audit log
|
||||
if (req.user) {
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: req.user.id,
|
||||
action: 'USER_LOGOUT',
|
||||
resource: 'user',
|
||||
resourceId: req.user.id,
|
||||
ipAddress: req.ip ?? null,
|
||||
userAgent: req.get('User-Agent') ?? null,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ message: '登出成功' });
|
||||
},
|
||||
|
||||
// 获取当前用户信息
|
||||
async getCurrentUser(req: AuthRequest, res: Response) {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: '未授权' });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user.id },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
isAdmin: true,
|
||||
isActive: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
user: user
|
||||
});
|
||||
},
|
||||
|
||||
// 刷新token
|
||||
async refreshToken(req: Request, res: Response) {
|
||||
const { refreshToken } = req.body;
|
||||
|
||||
if (!refreshToken) {
|
||||
return res.status(400).json({ error: '刷新令牌是必需的' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET!) as any;
|
||||
|
||||
const session = await prisma.session.findFirst({
|
||||
where: {
|
||||
token: refreshToken,
|
||||
expiresAt: {
|
||||
gt: new Date()
|
||||
}
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!session || !session.user || !session.user.isActive) {
|
||||
return res.status(401).json({ error: '无效的刷新令牌' });
|
||||
}
|
||||
|
||||
// Generate new token
|
||||
const newToken = generateToken(session.user.id);
|
||||
|
||||
// Update session
|
||||
await prisma.session.update({
|
||||
where: { id: session.id },
|
||||
data: {
|
||||
token: newToken,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({
|
||||
token: newToken,
|
||||
user: session.user
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: '无效的刷新令牌' });
|
||||
}
|
||||
},
|
||||
|
||||
// 调试端点 - 检查用户session状态
|
||||
async debugSession(req: AuthRequest, res: Response) {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: '未授权' });
|
||||
}
|
||||
|
||||
try {
|
||||
const sessions = await prisma.session.findMany({
|
||||
where: { userId: req.user.id },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
return res.json({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
sessions: sessions.map((s: any) => ({
|
||||
id: s.id,
|
||||
token: s.token.substring(0, 20) + '...',
|
||||
expiresAt: s.expiresAt,
|
||||
createdAt: s.createdAt,
|
||||
isExpired: s.expiresAt < new Date()
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: '获取session信息失败' });
|
||||
}
|
||||
}
|
||||
};
|
||||
365
backend/src/controllers/userController.ts
Normal file
365
backend/src/controllers/userController.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { prisma } from '../config/database';
|
||||
import { AuthRequest } from '../middleware/auth';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export const userController = {
|
||||
// 获取所有用户 (管理员)
|
||||
async getAllUsers(req: Request, res: Response) {
|
||||
const { page = 1, limit = 10, search = '', role = '' } = req.query;
|
||||
|
||||
const pageNum = parseInt(page as string) || 1;
|
||||
const limitNum = parseInt(limit as string) || 10;
|
||||
const skip = (pageNum - 1) * limitNum;
|
||||
|
||||
// 构建查询条件
|
||||
const where: any = {};
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ username: { contains: search as string, mode: 'insensitive' } },
|
||||
{ firstName: { contains: search as string, mode: 'insensitive' } },
|
||||
{ lastName: { contains: search as string, mode: 'insensitive' } }
|
||||
];
|
||||
}
|
||||
|
||||
if (role) {
|
||||
if (role === 'admin') {
|
||||
where.isAdmin = true;
|
||||
} else if (role === 'user') {
|
||||
where.isAdmin = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
const total = await prisma.user.count({ where });
|
||||
|
||||
// 获取用户列表
|
||||
const users = await prisma.user.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
isAdmin: true,
|
||||
isActive: true,
|
||||
totpEnabled: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
accountAssignments: {
|
||||
select: {
|
||||
accountId: true
|
||||
}
|
||||
}
|
||||
},
|
||||
skip,
|
||||
take: limitNum,
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
// 计算分页信息
|
||||
const totalPages = Math.ceil(total / limitNum);
|
||||
|
||||
return res.json({
|
||||
users: users.map((user: any) => ({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
role: user.isAdmin ? 'admin' : 'user',
|
||||
isAdmin: user.isAdmin,
|
||||
isActive: user.isActive,
|
||||
totpEnabled: user.totpEnabled,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
createdAt: user.createdAt,
|
||||
accounts: user.accountAssignments.map((assignment: any) => assignment.accountId)
|
||||
})),
|
||||
pagination: {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
total,
|
||||
totalPages
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 根据ID获取用户信息
|
||||
async getUserById(req: AuthRequest, res: Response) {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: '用户ID是必需的' });
|
||||
}
|
||||
|
||||
// 检查权限:只能查看自己的信息或管理员可以查看所有
|
||||
if (req.user?.id !== id && req.user?.role !== 'admin') {
|
||||
return res.status(403).json({ error: '权限不足' });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
isAdmin: true,
|
||||
isActive: true,
|
||||
totpEnabled: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
isAdmin: user.isAdmin,
|
||||
isActive: user.isActive,
|
||||
totpEnabled: user.totpEnabled,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
createdAt: user.createdAt
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
async updateUser(req: AuthRequest, res: Response) {
|
||||
const { id } = req.params;
|
||||
const { username, role, firstName, lastName, isActive, loginAttempts } = req.body;
|
||||
|
||||
console.log('收到更新请求:', req.body);
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: '用户ID是必需的' });
|
||||
}
|
||||
|
||||
// 检查权限:只能更新自己的信息或管理员可以更新所有
|
||||
if (req.user?.id !== id && req.user?.role !== 'admin') {
|
||||
return res.status(403).json({ error: '权限不足' });
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
const updateData: any = {};
|
||||
|
||||
// 只有管理员可以修改这些字段
|
||||
if (req.user?.role === 'admin') {
|
||||
if (username !== undefined) updateData.username = username;
|
||||
if (role !== undefined) updateData.isAdmin = role === 'admin';
|
||||
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() !== '') {
|
||||
console.log('正在处理密码更新');
|
||||
const hashedPassword = await bcrypt.hash(req.body.password, 12);
|
||||
updateData.password = hashedPassword;
|
||||
console.log('密码已加密');
|
||||
}
|
||||
updateData.loginAttempts = 0
|
||||
console.log('最终更新数据:', updateData);
|
||||
|
||||
// 检查用户名是否已存在(排除当前用户)
|
||||
if (username) {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
id: { not: id }
|
||||
}
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: '用户名已存在' });
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
isAdmin: true,
|
||||
isActive: true,
|
||||
totpEnabled: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
password: true,
|
||||
loginAttempts: true
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
isAdmin: user.isAdmin,
|
||||
isActive: user.isActive,
|
||||
totpEnabled: user.totpEnabled,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
createdAt: user.createdAt,
|
||||
password: user.password,
|
||||
loginAttempts: loginAttempts
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 更新用户账号权限
|
||||
async updateUserAccounts(req: AuthRequest, res: Response) {
|
||||
const { id } = req.params;
|
||||
const { accountIds } = req.body;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: '用户ID是必需的' });
|
||||
}
|
||||
|
||||
if (!Array.isArray(accountIds)) {
|
||||
return res.status(400).json({ error: 'accountIds必须是数组' });
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
// 检查所有账号是否存在
|
||||
const accounts = await prisma.websiteAccount.findMany({
|
||||
where: {
|
||||
id: { in: accountIds }
|
||||
}
|
||||
});
|
||||
|
||||
if (accounts.length !== accountIds.length) {
|
||||
return res.status(400).json({ error: '部分账号不存在' });
|
||||
}
|
||||
|
||||
// 使用事务来确保数据一致性
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// 删除用户现有的所有账号分配
|
||||
await tx.accountAssignment.deleteMany({
|
||||
where: { userId: id }
|
||||
});
|
||||
|
||||
// 创建新的账号分配
|
||||
if (accountIds.length > 0) {
|
||||
await tx.accountAssignment.createMany({
|
||||
data: accountIds.map(accountId => ({
|
||||
userId: id,
|
||||
accountId: accountId,
|
||||
isActive: true
|
||||
}))
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({
|
||||
message: '用户账号权限更新成功',
|
||||
accountIds
|
||||
});
|
||||
},
|
||||
|
||||
// 删除用户 (管理员)
|
||||
async deleteUser(req: Request, res: Response) {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: '用户ID是必需的' });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
// 删除用户相关的所有数据
|
||||
await prisma.$transaction([
|
||||
prisma.session.deleteMany({ where: { userId: id } }),
|
||||
prisma.accountAssignment.deleteMany({ where: { userId: id } }),
|
||||
prisma.auditLog.deleteMany({ where: { userId: id } }),
|
||||
prisma.user.delete({ where: { id } })
|
||||
]);
|
||||
|
||||
return res.json({ message: '用户删除成功' });
|
||||
},
|
||||
|
||||
// 创建用户 (管理员)
|
||||
async createUser(req: Request, res: Response) {
|
||||
const { username, password, role } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: '用户名和密码是必需的' });
|
||||
}
|
||||
|
||||
// 检查用户是否已存在
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: '用户名已存在' });
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
// 创建用户,默认为禁用状态
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
isAdmin: role === 'admin',
|
||||
isActive: false, // 新创建的用户默认为禁用状态
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
isAdmin: true,
|
||||
isActive: true,
|
||||
totpEnabled: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
message: '用户创建成功,需要激活后才能登录',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
role: user.isAdmin ? 'admin' : 'user',
|
||||
isAdmin: user.isAdmin,
|
||||
isActive: user.isActive,
|
||||
totpEnabled: user.totpEnabled,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
createdAt: user.createdAt
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
105
backend/src/index.ts
Normal file
105
backend/src/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'express-async-errors';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import dotenv from 'dotenv';
|
||||
import { logger } from './utils/logger';
|
||||
import { errorHandler } from './middleware/errorHandler';
|
||||
import { notFoundHandler } from './middleware/notFoundHandler';
|
||||
import authRoutes from './routes/auth';
|
||||
import userRoutes from './routes/users';
|
||||
import accountRoutes from './routes/accounts';
|
||||
import adminRoutes from './routes/admin';
|
||||
import { connectDatabase } from './config/database';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// 信任代理,确保正确获取客户端IP地址
|
||||
app.set('trust proxy', true);
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({
|
||||
origin: process.env.NODE_ENV === 'production'
|
||||
? ['http://frontend:3000',"http://backend:3001"]
|
||||
: ['http://localhost:3000', 'http://localhost:5173'],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes
|
||||
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.'
|
||||
}
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// Body parsing middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Request logging
|
||||
app.use((req, res, next) => {
|
||||
logger.info(`${req.method} ${req.path}`, {
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent')
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime()
|
||||
});
|
||||
});
|
||||
|
||||
// API routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/accounts', accountRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
|
||||
// Error handling middleware
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
// Start server
|
||||
async function startServer() {
|
||||
try {
|
||||
// Connect to database
|
||||
await connectDatabase();
|
||||
logger.info('Database connected successfully');
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
logger.info(`Server running on port ${PORT}`);
|
||||
logger.info(`Environment: ${process.env.NODE_ENV}`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('SIGTERM received, shutting down gracefully');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('SIGINT received, shutting down gracefully');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
startServer();
|
||||
19
backend/src/middleware/adminMiddleware.ts
Normal file
19
backend/src/middleware/adminMiddleware.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Response, NextFunction } from 'express';
|
||||
import { AuthRequest } from './authMiddleware';
|
||||
import { AppError } from './errorHandler';
|
||||
|
||||
export function adminMiddleware(
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (!req.user) {
|
||||
throw new AppError('Authentication required', 401);
|
||||
}
|
||||
|
||||
if (!req.user.isAdmin) {
|
||||
throw new AppError('Admin privileges required', 403);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
82
backend/src/middleware/auth.ts
Normal file
82
backend/src/middleware/auth.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { prisma } from '../config/database';
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 验证JWT token
|
||||
export const authenticateToken = async (
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: '未提供认证token'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
|
||||
|
||||
// 检查用户是否存在
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId }
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: '用户不存在或已被禁用'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
req.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.isAdmin ? 'admin' : 'user'
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: '无效的token'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 检查角色权限
|
||||
export const requireRole = (role: string) => {
|
||||
return (req: AuthRequest, res: Response, next: NextFunction): void => {
|
||||
if (!req.user) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: '需要认证'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.role !== role) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: '权限不足'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
72
backend/src/middleware/authMiddleware.ts
Normal file
72
backend/src/middleware/authMiddleware.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { prisma } from '../config/database';
|
||||
import { AppError } from './errorHandler';
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: {
|
||||
id: string;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export async function authMiddleware(
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new AppError('No token provided', 401);
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
console.log('验证token:', { token: token.substring(0, 20) + '...' })
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
|
||||
console.log('JWT解码成功:', { userId: decoded.userId })
|
||||
|
||||
// Check if session exists and is valid
|
||||
const session = await prisma.session.findFirst({
|
||||
where: {
|
||||
token,
|
||||
expiresAt: {
|
||||
gt: new Date()
|
||||
}
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Session查询结果:', {
|
||||
found: !!session,
|
||||
expiresAt: session?.expiresAt,
|
||||
currentTime: new Date(),
|
||||
userActive: session?.user?.isActive
|
||||
})
|
||||
|
||||
if (!session || !session.user || !session.user.isActive) {
|
||||
throw new AppError('Invalid or expired token', 401);
|
||||
}
|
||||
|
||||
req.user = session.user;
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
next(new AppError('Invalid token', 401));
|
||||
} else {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
backend/src/middleware/errorHandler.ts
Normal file
45
backend/src/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export class AppError extends Error {
|
||||
public statusCode: number;
|
||||
public isOperational: boolean;
|
||||
|
||||
constructor(message: string, statusCode: number = 500) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.isOperational = true;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
export function errorHandler(
|
||||
error: AppError,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const statusCode = error.statusCode || 500;
|
||||
const message = error.message || 'Internal Server Error';
|
||||
|
||||
// Log error
|
||||
logger.error('Error occurred:', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent')
|
||||
});
|
||||
|
||||
// Don't leak error details in production
|
||||
const responseMessage = process.env.NODE_ENV === 'production' && statusCode === 500
|
||||
? 'Internal Server Error'
|
||||
: message;
|
||||
|
||||
res.status(statusCode).json({
|
||||
error: responseMessage,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: error.stack })
|
||||
});
|
||||
}
|
||||
9
backend/src/middleware/notFoundHandler.ts
Normal file
9
backend/src/middleware/notFoundHandler.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export function notFoundHandler(req: Request, res: Response) {
|
||||
res.status(404).json({
|
||||
error: 'Route not found',
|
||||
path: req.path,
|
||||
method: req.method
|
||||
});
|
||||
}
|
||||
16
backend/src/middleware/validateRequest.ts
Normal file
16
backend/src/middleware/validateRequest.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { validationResult } from 'express-validator';
|
||||
|
||||
export function validateRequest(req: Request, res: Response, next: NextFunction): void {
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
details: errors.array()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
17
backend/src/middleware/validation.ts
Normal file
17
backend/src/middleware/validation.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { validationResult } from 'express-validator';
|
||||
|
||||
export const validateRequest = (req: Request, res: Response, next: NextFunction): void => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
38
backend/src/routes/accounts.ts
Normal file
38
backend/src/routes/accounts.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Router } from 'express';
|
||||
import { accountController } from '../controllers/accountController';
|
||||
import { authMiddleware } from '../middleware/authMiddleware';
|
||||
import { adminMiddleware } from '../middleware/adminMiddleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Get all accounts (admin only)
|
||||
router.get('/', adminMiddleware, accountController.getAllAccounts);
|
||||
|
||||
// Get account by ID (admin only)
|
||||
router.get('/:id', adminMiddleware, accountController.getAccountById);
|
||||
|
||||
// Create new account (admin only)
|
||||
router.post('/', adminMiddleware, accountController.createAccount);
|
||||
|
||||
// Update account (admin only)
|
||||
router.put('/:id', adminMiddleware, accountController.updateAccount);
|
||||
|
||||
// Delete account (admin only)
|
||||
router.delete('/:id', adminMiddleware, accountController.deleteAccount);
|
||||
|
||||
// Get user's assigned accounts
|
||||
router.get('/user/assigned', accountController.getUserAccounts);
|
||||
|
||||
// Assign account to user (admin only)
|
||||
router.post('/:id/assign', adminMiddleware, accountController.assignAccount);
|
||||
|
||||
// Unassign account from user (admin only)
|
||||
router.delete('/:id/assign/:userId', adminMiddleware, accountController.unassignAccount);
|
||||
|
||||
// Login to website
|
||||
router.post('/:accountId/login', accountController.loginToWebsite);
|
||||
|
||||
export default router;
|
||||
37
backend/src/routes/admin.ts
Normal file
37
backend/src/routes/admin.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import express from 'express';
|
||||
import { authenticateToken, requireRole } from '../middleware/auth';
|
||||
import { validateRequest } from '../middleware/validation';
|
||||
import { adminController } from '../controllers/adminController';
|
||||
import { userController } from '../controllers/userController';
|
||||
import { accountController } from '../controllers/accountController';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 管理员认证中间件
|
||||
const requireAdmin = requireRole('admin');
|
||||
|
||||
// 管理员登录
|
||||
router.post('/login', validateRequest, adminController.login);
|
||||
|
||||
// 获取统计数据
|
||||
router.get('/stats', authenticateToken, requireAdmin, adminController.getStats);
|
||||
|
||||
// 获取最近活动
|
||||
router.get('/activities', authenticateToken, requireAdmin, adminController.getRecentActivities);
|
||||
|
||||
// 用户管理
|
||||
router.get('/users', authenticateToken, requireAdmin, userController.getAllUsers);
|
||||
router.post('/users', authenticateToken, requireAdmin, validateRequest, userController.createUser);
|
||||
router.get('/users/:id', authenticateToken, requireAdmin, userController.getUserById);
|
||||
router.put('/users/:id', authenticateToken, requireAdmin, validateRequest, userController.updateUser);
|
||||
router.put('/users/:id/accounts', authenticateToken, requireAdmin, validateRequest, userController.updateUserAccounts);
|
||||
router.delete('/users/:id', authenticateToken, requireAdmin, userController.deleteUser);
|
||||
|
||||
// 账号管理
|
||||
router.get('/accounts', authenticateToken, requireAdmin, accountController.getAllAccounts);
|
||||
router.post('/accounts', authenticateToken, requireAdmin, validateRequest, accountController.createAccount);
|
||||
router.get('/accounts/:id', authenticateToken, requireAdmin, accountController.getAccountById);
|
||||
router.put('/accounts/:id', authenticateToken, requireAdmin, validateRequest, accountController.updateAccount);
|
||||
router.delete('/accounts/:id', authenticateToken, requireAdmin, accountController.deleteAccount);
|
||||
|
||||
export default router;
|
||||
52
backend/src/routes/auth.ts
Normal file
52
backend/src/routes/auth.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Router } from 'express';
|
||||
import { body } from 'express-validator';
|
||||
import { validateRequest } from '../middleware/validateRequest';
|
||||
import { authController } from '../controllers/authController';
|
||||
import { authMiddleware } from '../middleware/authMiddleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Register
|
||||
router.post('/register', [
|
||||
body('username').isLength({ min: 3, max: 30 }).matches(/^[a-zA-Z0-9_]+$/),
|
||||
body('password').isLength({ min: 8 }),
|
||||
body('confirmPassword').isLength({ min: 8 }).custom((value, { req }) => {
|
||||
if (value !== req.body.password) {
|
||||
throw new Error('密码和确认密码不匹配');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
body('firstName').optional().isLength({ max: 50 }),
|
||||
body('lastName').optional().isLength({ max: 50 }),
|
||||
validateRequest
|
||||
], authController.register);
|
||||
|
||||
// Login
|
||||
router.post('/login', [
|
||||
body('username').isLength({ min: 3, max: 30 }),
|
||||
body('password').notEmpty(),
|
||||
validateRequest
|
||||
], authController.login);
|
||||
|
||||
// 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
|
||||
router.get('/me', authMiddleware, authController.getCurrentUser);
|
||||
|
||||
// Refresh token
|
||||
router.post('/refresh', authController.refreshToken);
|
||||
|
||||
// Debug session (for development)
|
||||
router.get('/debug-session', authMiddleware, authController.debugSession);
|
||||
|
||||
export default router;
|
||||
23
backend/src/routes/users.ts
Normal file
23
backend/src/routes/users.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Router } from 'express';
|
||||
import { userController } from '../controllers/userController';
|
||||
import { authMiddleware } from '../middleware/authMiddleware';
|
||||
import { adminMiddleware } from '../middleware/adminMiddleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Get all users (admin only)
|
||||
router.get('/', adminMiddleware, userController.getAllUsers);
|
||||
|
||||
// Get user by ID (admin or self)
|
||||
router.get('/:id', userController.getUserById);
|
||||
|
||||
// Update user (admin or self)
|
||||
router.put('/:id', userController.updateUser);
|
||||
|
||||
// Delete user (admin only)
|
||||
router.delete('/:id', adminMiddleware, userController.deleteUser);
|
||||
|
||||
export default router;
|
||||
41
backend/src/utils/audit.ts
Normal file
41
backend/src/utils/audit.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { prisma } from '../config/database';
|
||||
|
||||
interface AuditLogData {
|
||||
userId: string | null;
|
||||
action: string;
|
||||
resource: string;
|
||||
resourceId?: string;
|
||||
details?: any;
|
||||
ipAddress?: string | null;
|
||||
userAgent?: string | null;
|
||||
}
|
||||
|
||||
export const createAuditLog = async (data: AuditLogData) => {
|
||||
try {
|
||||
const auditData: any = {
|
||||
userId: data.userId,
|
||||
action: data.action,
|
||||
resource: data.resource
|
||||
};
|
||||
|
||||
// 只添加非undefined的字段
|
||||
if (data.resourceId !== undefined) {
|
||||
auditData.resourceId = data.resourceId;
|
||||
}
|
||||
if (data.details !== undefined) {
|
||||
auditData.details = data.details;
|
||||
}
|
||||
if (data.ipAddress !== undefined) {
|
||||
auditData.ipAddress = data.ipAddress;
|
||||
}
|
||||
if (data.userAgent !== undefined) {
|
||||
auditData.userAgent = data.userAgent;
|
||||
}
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: auditData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create audit log:', error);
|
||||
}
|
||||
};
|
||||
26
backend/src/utils/logger.ts
Normal file
26
backend/src/utils/logger.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import winston from 'winston';
|
||||
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
);
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
format: logFormat,
|
||||
defaultMeta: { service: 'pandora-backend' },
|
||||
transports: [
|
||||
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'logs/combined.log' }),
|
||||
],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
30
backend/tsconfig.json
Normal file
30
backend/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user