first commit

This commit is contained in:
2025-07-08 00:52:10 +08:00
commit aa2416c5d6
69 changed files with 16628 additions and 0 deletions

38
backend/Dockerfile Normal file
View 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
View 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
View 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"
}
}

View 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
View 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();
});

View 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;
}
}

View 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;
}
}

View 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: '获取最近活动失败'
});
}
}
};

View 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: '获取审计日志失败'
});
}
}
};

View 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信息失败' });
}
}
};

View 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
View 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();

View 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();
}

View 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();
};
};

View 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);
}
}
}

View 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 })
});
}

View 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
});
}

View 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();
}

View 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();
};

View 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;

View 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;

View 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;

View 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;

View 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);
}
};

View 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
View 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"
]
}