diff --git a/.env.example b/.env.example index 46b1afd..eaedd14 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,13 @@ # 数据库连接 DATABASE_URL="postgresql://postgres:password@localhost:5432/maqt?schema=public" -# JWT 密钥(请修改为随机字符串) -JWT_SECRET="your-super-secret-jwt-key-change-this-in-production" +# JWT 密钥(务必修改为随机长字符串) +JWT_SECRET="change-me-to-a-random-64-char-string" # JWT 过期时间 JWT_EXPIRES_IN="7d" -# 数据加密密钥(32字节,请修改) +# 数据加密密钥(32字节,务必修改) ENCRYPTION_KEY="0123456789abcdef0123456789abcdef" # 服务端口 @@ -16,5 +16,8 @@ PORT=3001 # 环境 NODE_ENV="development" -# VIP 卡密批次密钥(用于生成卡密签名) -BATCH_SECRET="your-batch-secret-key" +# CORS 允许的来源 (逗号分隔) +ALLOWED_ORIGINS="http://localhost:5173,app://.,file://" + +# 管理员密钥 (用于 /api/admin 端点) +ADMIN_SECRET="change-me-to-random" diff --git a/API_FULL_ANALYSIS.md b/API_FULL_ANALYSIS.md new file mode 100644 index 0000000..8e18e63 --- /dev/null +++ b/API_FULL_ANALYSIS.md @@ -0,0 +1,192 @@ +# 码枪堂 API 全线分析报告 + +> 基于 Fiddler 抓包 ss3.saz(原版 API)+ ss4.saz(自定义服务器)对比 + +--- + +## 一、API 域名概览 + +| 域名 | 用途 | +|------|------| +| `https://maqt.top/api/*` | 主业务 API | +| `https://pop.maqt.top/api/*` | 弹窗广告 API | +| `https://update.maqt.top/*` | 更新服务 | +| `https://tuku.maqt.top/*` | 图片托管 | +| `http://100.105.17.52:3001/api/*` | 自定义后端(解包版) | + +--- + +## 二、完整 API 端点清单 + +### 🔐 认证 +| 方法 | 端点 | 请求体 | 当前状态 | +|------|------|--------|----------| +| POST | `/api/login` | `{"username","password","installId","deviceHash","platform","osVersion","appVersion"}` | ✅ 已实现 | +| POST | `/api/register` | 需要抓包确认 | ❌ 待实现 | +| POST | `/api/activate-vip` | 需要抓包确认 | ✅ 已实现 | + +### 📊 会话 & 状态 +| 方法 | 端点 | 当前状态 | +|------|------|----------| +| GET | `/api/session-status` | ✅ 已实现 | +| GET | `/api/vip-status` | ❌ 待实现 | +| GET | `/api/user/stats/{userId}` | ❌ 待实现 | +| GET | `/api/user/favorited-count/{userId}` | ❌ 待实现 | +| GET | `/api/user/limits/{userId}` | ❌ 待实现 | +| GET | `/api/user/schemes/{userId}` | ❌ 待实现 | + +### 🎯 改枪方案 +| 方法 | 端点 | 参数 | 当前状态 | +|------|------|------|----------| +| GET | `/api/category/{type}` | AR/SMG/SR/LMG/SG/Pistol/Launcher | ✅ 已实现 | +| GET | `/api/schemes` | `sort=hot&page=1&limit=12&source=...` | ✅ 已实现 | +| GET | `/api/schemes` | `sort=hot&page=1&limit=12&weaponCategory&weaponName` | ✅ 已实现 | +| POST | `/api/schemes/{id}/use` | 需要抓包确认 | ❌ 待实现 | + +### ❤️ 收藏 +| 方法 | 端点 | 当前状态 | +|------|------|----------| +| GET | `/api/favorites/count` | ✅ 已实现 | +| GET | `/api/favorites/check?schemeId={id}&source=...` | ✅ 已实现 | + +### 🎯 弹出广告 +| 方法 | 端点 | 当前状态 | +|------|------|----------| +| GET | `/api/popups/test_beta_01` | ❌ 待实现 | +| GET | `/api/aftersale-tutorial-popup` | ❌ 待实现 | +| GET | `/api/software-version-ad` | ❌ 待实现 | + +### 🔄 更新服务 +| 方法 | 端点 | 备注 | +|------|------|------| +| GET | `/latest.yml` | Electron auto-updater 配置 | +| GET | `/update-config.json` | 更新配置 | + +### 🖼️ 图片托管 +| 方法 | 端点 | 备注 | +|------|------|------| +| GET | `tuku.maqt.top/i/2026/03/22/*.png` | 静态图片资源 | + +--- + +## 三、ss3 vs ss4 请求对比(关键差异) + +### 原版(ss3)完整流程: +``` +1. 检查更新: GET update.maqt.top/{latest.yml,update-config.json} +2. 弹窗广告: GET pop.maqt.top/api/software-version-ad +3. 会话状态: GET maqt.top/api/session-status (高频轮询!) +4. 注册/登录: POST maqt.top/api/register +5. 获取分类: GET maqt.top/api/category/{type} +6. 获取方案: GET maqt.top/api/schemes?... +7. 收藏检查: GET maqt.top/api/favorites/check?schemeId=... +8. 激活VIP: POST maqt.top/api/activate-vip +9. VIP状态: GET maqt.top/api/vip-status +10. 用户数据: GET maqt.top/api/user/{stats,favorited-count,limits,schemes}/{id} +11. 方案使用: POST maqt.top/api/schemes/{id}/use +12. 图片加载: GET tuku.maqt.top/... (持续轮询) +``` + +### 自定义服务器(ss4 后半段)请求序列: +``` +162: GET /api/popups/test_beta_01 +163: GET /api/session-status +164: GET /api/software-version-ad +165: GET /api/aftersale-tutorial-popup +166: GET /update-config.json +169: GET /api/software-version-ad +170: GET /api/popups/test_beta_01 +171: GET /api/aftersale-tutorial-popup +172: GET /update-config.json +173: GET /api/category/AR +174: GET /api/schemes?sort=hot&page=1&limit=12&source=... +175: GET /api/category/SMG +176: GET /api/category/SR +177: GET /api/category/LMG +178: GET /api/category/SG +179: GET /api/category/Pistol +180: GET /api/category/Launcher +181: POST /api/login +182: GET /api/session-status +183: GET /api/favorites/count +184: GET /api/schemes?... +185: GET /api/schemes?... +186: GET /api/favorites/count +187: GET /api/session-status +190: GET /api/session-status +194-199: GET /api/session-status (轮询...) +``` + +### ❗ 自定义服务器缺失的端点 +以下端点在原版中有调用,但在自定义服务器请求中从未出现: +- **POST `/api/register`** — 注册功能 +- **POST `/api/activate-vip`** — VIP 激活(端点存在但可能未被调用) +- **GET `/api/vip-status`** — VIP 状态查询 +- **GET `/api/user/stats/{id}`** — 用户统计 +- **GET `/api/user/favorited-count/{id}`** — 用户收藏数 +- **GET `/api/user/limits/{id}`** — 用户限制 +- **GET `/api/user/schemes/{id}`** — 用户方案列表 +- **POST `/api/schemes/{id}/use`** — 方案使用记录 + +--- + +## 四、重新封装开发计划 + +### 阶段 1:API 补充(先补齐缺失端点) + +**优先级 P0(应用启动必需):** +- [ ] `GET /api/vip-status` — 登录后立刻调用,缺了前端一直没 VIP +- [ ] `GET /api/user/stats/:id` +- [ ] `GET /api/user/favorited-count/:id` +- [ ] `GET /api/user/limits/:id` +- [ ] `GET /api/user/schemes/:id` +- [ ] `POST /api/schemes/:id/use` + +**优先级 P1(弹窗广告,不影响核心功能):** +- [ ] `GET /api/software-version-ad` +- [ ] `GET /api/popups/test_beta_01` +- [ ] `GET /api/aftersale-tutorial-popup` +- [ ] `GET /api/update-config.json` + +**优先级 P2(注册,一次性的):** +- [ ] `POST /api/register`(含 installId, deviceHash) + +### 阶段 2:桌面端复刻(Electron + React) + +等 API 补全后,桌面端从头写: + +``` +maqt-desktop/ +├── electron/ +│ ├── main.ts # 窗口管理 +│ └── preload.ts # electronAPI +├── src/ +│ ├── App.tsx +│ ├── pages/ +│ │ ├── Home.tsx # 桌面首页 +│ │ ├── Login.tsx +│ │ ├── Schemes.tsx # 改枪方案 +│ │ ├── Filters.tsx # 画面滤镜 +│ │ ├── Optimization.tsx +│ │ └── ... +│ ├── components/ +│ │ ├── Dock.tsx # 底部导航 +│ │ ├── DesktopIcons.tsx +│ │ └── ... +│ └── hooks/ +│ ├── useAuth.ts +│ └── useApi.ts +├── native/ # 原生程序 +│ ├── MaqiangTangh1.exe +│ ├── nvidiaProfileInspector.exe +│ └── tools/ +└── package.json +``` + +### 阶段 3:原生功能对接 + +- [ ] spawn `MaqiangTangh1.exe` 执行系统优化 +- [ ] spawn `MaqiangTangXiXiOverlay.exe` 游戏内 Overlay +- [ ] 调用 PowerShell 脚本 Gamma 校准 +- [ ] NVIDIA Profile Inspector 配置 +- [ ] 硬件监控面板 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f065a2a..8990871 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,6 +24,9 @@ model User { isVip Boolean @default(false) @map("is_vip") vipLevel Int @default(0) @map("vip_level") vipExpireAt DateTime? @map("vip_expire_at") + + // Token 吊销 (递增版本号使旧 token 失效) + tokenVersion Int @default(0) @map("token_version") // 统计 schemesCount Int @default(0) @map("schemes_count") diff --git a/prisma/seed.ts b/prisma/seed.ts index 3b9847e..aa3077e 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -4,15 +4,17 @@ import crypto from 'crypto'; const prisma = new PrismaClient(); /** - * 生成 VIP 卡密 + * 生成 VIP 卡密 (原始 + 哈希) */ -function generateCardKey(days: number): string { +function generateCardKey(days: number): { raw: string; hash: string } { const prefix = `VIP${days}`; const segments = []; for (let i = 0; i < 4; i++) { segments.push(crypto.randomBytes(2).toString('hex').toUpperCase()); } - return `${prefix}-${segments.join('-')}`; + const raw = `${prefix}-${segments.join('-')}`; + const hash = crypto.createHash('sha256').update(raw).digest('hex'); + return { raw, hash }; } async function main() { @@ -22,79 +24,59 @@ async function main() { // 创建分类 // ============================================ const categories = [ - // 烽火地带 { name: '突击步枪', type: 'SCHEME' }, { name: '冲锋枪', type: 'SCHEME' }, { name: '狙击步枪', type: 'SCHEME' }, { name: '轻机枪', type: 'SCHEME' }, { name: '霰弹枪', type: 'SCHEME' }, { name: '手枪', type: 'SCHEME' }, - - // 全面战场 + { name: '发射器', type: 'SCHEME' }, { name: '突击步枪', type: 'SCHEME_AOB' }, { name: '冲锋枪', type: 'SCHEME_AOB' }, { name: '狙击步枪', type: 'SCHEME_AOB' }, { name: '轻机枪', type: 'SCHEME_AOB' }, { name: '霰弹枪', type: 'SCHEME_AOB' }, - - // 滤镜 { name: '烽火地带', type: 'FILTER' }, { name: '全面战场', type: 'FILTER' }, { name: '通用', type: 'FILTER' }, ]; for (const cat of categories) { - await prisma.category.create({ - data: cat, - }).catch(() => {}); // 忽略重复错误 + await prisma.category.create({ data: cat }).catch(() => {}); } console.log(`✅ 创建 ${categories.length} 个分类`); // ============================================ - // 生成 VIP 卡密 + // 生成 VIP 卡密 (存储哈希,输出原始值仅此一次) // ============================================ + const testKey = { raw: 'VIP365-0000-0000-0000-0000', hash: crypto.createHash('sha256').update('VIP365-0000-0000-0000-0000').digest('hex') }; + + await prisma.vipCard.create({ + data: { cardKey: testKey.hash, cardType: 'YEAR', days: 365 }, + }).catch(() => {}); + console.log(`\n🔑 测试卡密 (仅显示一次): ${testKey.raw}`); + const cardConfigs = [ { type: 'MONTH', days: 30, count: 100 }, { type: 'QUARTER', days: 90, count: 50 }, { type: 'YEAR', days: 365, count: 20 }, ]; - // 测试卡密 (固定值,方便开发验证) - await prisma.vipCard.create({ - data: { - cardKey: 'VIP365-0000-0000-0000-0000', - cardType: 'YEAR', - days: 365, - }, - }).catch(() => {}); - console.log(`✅ 创建测试卡密: VIP365-0000-0000-0000-0000`); - let totalCards = 0; for (const config of cardConfigs) { const cards = []; + console.log(`\n📦 ${config.type} 卡密 (${config.count} 张):`); for (let i = 0; i < config.count; i++) { - cards.push({ - cardKey: generateCardKey(config.days), - cardType: config.type, - days: config.days, - }); + const { raw, hash } = generateCardKey(config.days); + cards.push({ cardKey: hash, cardType: config.type, days: config.days }); + console.log(` ${raw}`); } - await prisma.vipCard.createMany({ data: cards, skipDuplicates: true }); totalCards += config.count; - console.log(`✅ 生成 ${config.type} 卡密 ${config.count} 张`); } console.log(`\n🎉 种子数据初始化完成!`); + console.log(` ⚠️ 以上卡密仅显示一次,请妥善保存`); console.log(` - 分类:${categories.length} 个`); - console.log(` - VIP 卡密:${totalCards} 张`); + console.log(` - VIP 卡密:${totalCards + 1} 张`); } - -main() - .catch((e) => { - console.error('❌ 种子数据初始化失败:', e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/src/index.ts b/src/index.ts index 7659f1d..4f97045 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,9 +27,21 @@ app.use(helmet({ crossOriginEmbedderPolicy: false, })); -// CORS +// CORS — 仅允许白名单来源 +const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') || [ + 'http://localhost:5173', + 'http://localhost:3000', + 'app://.', + 'file://', +]; app.use(cors({ - origin: true, // 允许所有来源(开发模式) + origin: (origin, callback) => { + if (!origin || ALLOWED_ORIGINS.some(o => origin.startsWith(o))) { + callback(null, true); + } else { + callback(new Error('CORS blocked')); + } + }, credentials: true, })); @@ -221,11 +233,11 @@ releaseDate: '2024-01-01T00:00:00.000Z' }); // ============================================ -// 开发工具端点 (仅 NODE_ENV=development) -// ============================================ +// 开发工具端点 (需 ADMIN_SECRET) app.post('/api/admin/set-vip', async (req, res) => { - if (process.env.NODE_ENV !== 'development') { - return res.status(403).json({ success: false, message: '仅开发环境可用' }); + const secret = process.env.ADMIN_SECRET; + if (!secret || req.headers['x-admin-secret'] !== secret) { + return res.status(403).json({ success: false, message: '禁止访问' }); } try { const { username, isVip } = req.body; diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index a6f64c0..c9f3c16 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -44,12 +44,12 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc }); } - // 检查用户是否存在且未被删除 + // 检查用户是否存在且未被删除,同时验证 tokenVersion 一致 const user = await prisma.user.findUnique({ where: { id: payload.userId }, - select: { id: true, deletedAt: true }, + select: { id: true, deletedAt: true, tokenVersion: true }, }); - + if (!user || user.deletedAt) { return res.status(401).json({ success: false, @@ -57,6 +57,15 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc code: 'USER_NOT_FOUND', }); } + + // token 已被吊销 (登出或密码修改后旧 token 失效) + if (user.tokenVersion !== payload.tokenVersion) { + return res.status(401).json({ + success: false, + message: '访问令牌已失效,请重新登录', + code: 'TOKEN_EXPIRED', + }); + } // 将用户信息附加到请求对象 req.user = payload; diff --git a/src/routes/auth.ts b/src/routes/auth.ts index d97a0bc..5e61b14 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -13,7 +13,10 @@ const prisma = new PrismaClient(); // ============================================ const registerSchema = z.object({ username: z.string().min(3).max(50), - password: z.string().min(6).max(100), + password: z.string().min(8, '密码至少8位').max(100) + .regex(/[A-Z]/, '密码需包含大写字母') + .regex(/[a-z]/, '密码需包含小写字母') + .regex(/[0-9]/, '密码需包含数字'), email: z.string().email(), installId: z.string().optional(), deviceHash: z.string().optional(), @@ -22,34 +25,34 @@ const registerSchema = z.object({ router.post('/register', async (req: Request, res: Response) => { try { const body = registerSchema.parse(req.body); - + // 检查用户名是否已存在 const existingUsername = await prisma.user.findUnique({ where: { username: body.username }, }); - + if (existingUsername) { return res.status(400).json({ success: false, message: '用户名已被使用', }); } - + // 检查邮箱是否已存在 const existingEmail = await prisma.user.findUnique({ where: { email: body.email }, }); - + if (existingEmail) { return res.status(400).json({ success: false, message: '邮箱已被注册', }); } - + // 密码哈希 const passwordHash = await bcrypt.hash(body.password, 10); - + // 创建用户 const user = await prisma.user.create({ data: { @@ -60,7 +63,7 @@ router.post('/register', async (req: Request, res: Response) => { deviceHash: body.deviceHash, }, }); - + // 生成 Token const token = generateToken({ userId: user.id, @@ -68,8 +71,9 @@ router.post('/register', async (req: Request, res: Response) => { email: user.email, isVip: user.isVip, vipLevel: user.vipLevel, + tokenVersion: user.tokenVersion, }); - + // 记录日志 await prisma.userLog.create({ data: { @@ -80,7 +84,7 @@ router.post('/register', async (req: Request, res: Response) => { ipAddress: req.ip, }, }); - + res.json({ success: true, token, @@ -102,7 +106,7 @@ router.post('/register', async (req: Request, res: Response) => { errors: error.errors, }); } - + console.error('Register error:', error); res.status(500).json({ success: false, message: '注册失败' }); } @@ -170,8 +174,9 @@ router.post('/login', async (req: Request, res: Response) => { email: user.email, isVip: user.isVip, vipLevel: user.vipLevel, + tokenVersion: user.tokenVersion, }); - + // 记录日志 await prisma.userLog.create({ data: { @@ -182,7 +187,7 @@ router.post('/login', async (req: Request, res: Response) => { ipAddress: req.ip, }, }); - + res.json({ success: true, token, @@ -204,12 +209,28 @@ router.post('/login', async (req: Request, res: Response) => { errors: error.errors, }); } - + console.error('Login error:', error); res.status(500).json({ success: false, message: '登录失败' }); } }); +// ============================================ +// 登出 (使当前 token 失效) +// ============================================ +router.post('/logout', authMiddleware, async (req: Request, res: Response) => { + try { + await prisma.user.update({ + where: { id: req.user!.userId }, + data: { tokenVersion: { increment: 1 } }, + }); + res.json({ success: true, message: '已登出' }); + } catch (error) { + console.error('Logout error:', error); + res.status(500).json({ success: false, message: '登出失败' }); + } +}); + // ============================================ // 会话状态 (从数据库查询最新状态,避免 token 缓存导致 isVip 过期) // ============================================ diff --git a/src/routes/vip.ts b/src/routes/vip.ts index aec7222..491c412 100644 --- a/src/routes/vip.ts +++ b/src/routes/vip.ts @@ -1,5 +1,6 @@ import { Router, Request, Response } from 'express'; import { PrismaClient } from '@prisma/client'; +import crypto from 'crypto'; import { z } from 'zod'; import { authMiddleware } from '../middleware/auth'; import { generateToken } from '../utils/jwt'; @@ -7,6 +8,10 @@ import { generateToken } from '../utils/jwt'; const router = Router(); const prisma = new PrismaClient(); +function hashCardKey(raw: string): string { + return crypto.createHash('sha256').update(raw).digest('hex'); +} + // ============================================ // VIP 卡密激活 // ============================================ @@ -18,9 +23,10 @@ router.post('/activate-vip', authMiddleware, async (req: Request, res: Response) try { const body = activateSchema.parse(req.body); - // 查找卡密 + // 查找卡密 (比对哈希) + const hashedKey = hashCardKey(body.cardKey); const card = await prisma.vipCard.findUnique({ - where: { cardKey: body.cardKey }, + where: { cardKey: hashedKey }, }); if (!card) { @@ -97,6 +103,7 @@ router.post('/activate-vip', authMiddleware, async (req: Request, res: Response) email: updatedUser.email, isVip: true, vipLevel: updatedUser.vipLevel, + tokenVersion: updatedUser.tokenVersion, }); res.json({ diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index 80751a6..78f97c1 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -12,6 +12,7 @@ export interface JwtPayload { email: string; isVip: boolean; vipLevel: number; + tokenVersion: number; } /**