feat: production hardening - CORS whitelist, strong password, tokenVersion revoke, VIP card hashing, admin secret
This commit is contained in:
13
.env.example
13
.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"
|
||||
|
||||
192
API_FULL_ANALYSIS.md
Normal file
192
API_FULL_ANALYSIS.md
Normal file
@@ -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 配置
|
||||
- [ ] 硬件监控面板
|
||||
@@ -25,6 +25,9 @@ model User {
|
||||
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")
|
||||
favoritesCount Int @default(0) @map("favorites_count")
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
24
src/index.ts
24
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;
|
||||
|
||||
@@ -44,10 +44,10 @@ 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) {
|
||||
@@ -58,6 +58,15 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc
|
||||
});
|
||||
}
|
||||
|
||||
// token 已被吊销 (登出或密码修改后旧 token 失效)
|
||||
if (user.tokenVersion !== payload.tokenVersion) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '访问令牌已失效,请重新登录',
|
||||
code: 'TOKEN_EXPIRED',
|
||||
});
|
||||
}
|
||||
|
||||
// 将用户信息附加到请求对象
|
||||
req.user = payload;
|
||||
next();
|
||||
|
||||
@@ -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(),
|
||||
@@ -68,6 +71,7 @@ router.post('/register', async (req: Request, res: Response) => {
|
||||
email: user.email,
|
||||
isVip: user.isVip,
|
||||
vipLevel: user.vipLevel,
|
||||
tokenVersion: user.tokenVersion,
|
||||
});
|
||||
|
||||
// 记录日志
|
||||
@@ -170,6 +174,7 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
email: user.email,
|
||||
isVip: user.isVip,
|
||||
vipLevel: user.vipLevel,
|
||||
tokenVersion: user.tokenVersion,
|
||||
});
|
||||
|
||||
// 记录日志
|
||||
@@ -210,6 +215,22 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 登出 (使当前 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 过期)
|
||||
// ============================================
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface JwtPayload {
|
||||
email: string;
|
||||
isVip: boolean;
|
||||
vipLevel: number;
|
||||
tokenVersion: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user