commit b84f111e8fadde1b59eb12d00f1f579d84484d08 Author: Chen Gu Date: Sat May 9 00:52:04 2026 +0800 chore: mqsrv backend diff --git a/.env b/.env new file mode 100644 index 0000000..8678594 --- /dev/null +++ b/.env @@ -0,0 +1,20 @@ +# 数据库连接 +DATABASE_URL="postgresql://maqt:maqt123456@localhost:5432/maqt?schema=public" + +# JWT 密钥 +JWT_SECRET="maqt-jwt-secret-key-2026-change-in-production" + +# JWT 过期时间 +JWT_EXPIRES_IN="7d" + +# 数据加密密钥(32字节) +ENCRYPTION_KEY="maqt-encryption-key-32bytes!" + +# 服务端口 +PORT=3001 + +# 环境 +NODE_ENV="development" + +# VIP 卡密批次密钥 +BATCH_SECRET="maqt-batch-secret-2026" diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..46b1afd --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# 数据库连接 +DATABASE_URL="postgresql://postgres:password@localhost:5432/maqt?schema=public" + +# JWT 密钥(请修改为随机字符串) +JWT_SECRET="your-super-secret-jwt-key-change-this-in-production" + +# JWT 过期时间 +JWT_EXPIRES_IN="7d" + +# 数据加密密钥(32字节,请修改) +ENCRYPTION_KEY="0123456789abcdef0123456789abcdef" + +# 服务端口 +PORT=3001 + +# 环境 +NODE_ENV="development" + +# VIP 卡密批次密钥(用于生成卡密签名) +BATCH_SECRET="your-batch-secret-key" 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/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb0f66c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# 第一阶段: 构建 +FROM node:22-alpine AS builder +WORKDIR /app + +# 安装必要的系统依赖(Prisma 需要) +RUN apk add --no-cache openssl + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npx prisma generate +RUN npx tsc + +# 第二阶段: 运行 +FROM node:22-alpine +WORKDIR /app + +# 安装 OpenSSL(Prisma 运行时依赖) +RUN apk add --no-cache openssl + +COPY --from=builder /app/package*.json ./ +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/prisma ./prisma + +EXPOSE 3001 +CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb438fd --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# 码枪堂后端 API + +基于 Express + Prisma + PostgreSQL 的后端服务。 + +## 快速开始 + +### 1. 安装依赖 + +```bash +pnpm install +# 或 +npm install +``` + +### 2. 配置环境变量 + +```bash +cp .env.example .env +``` + +编辑 `.env` 文件: + +```env +# 数据库连接 +DATABASE_URL="postgresql://postgres:password@localhost:5432/maqt?schema=public" + +# JWT 密钥(请修改为随机字符串) +JWT_SECRET="your-super-secret-jwt-key-change-this-in-production" + +# 数据加密密钥(32字节) +ENCRYPTION_KEY="0123456789abcdef0123456789abcdef" + +# 服务端口 +PORT=3001 +``` + +### 3. 初始化数据库 + +```bash +# 生成 Prisma Client +pnpm db:generate + +# 同步数据库结构 +pnpm db:push + +# 生成种子数据(VIP卡密等) +pnpm db:seed +``` + +### 4. 启动服务 + +```bash +# 开发模式(热重载) +pnpm dev + +# 生产模式 +pnpm build +pnpm start +``` + +## API 接口 + +### 认证 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/api/register` | POST | 注册用户 | +| `/api/login` | POST | 用户登录 | +| `/api/session-status` | GET | 会话状态 | +| `/api/vip-status` | GET | VIP 状态 | +| `/api/activate-vip` | POST | VIP 卡密激活 | + +### 用户管理 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/api/user/username` | PUT | 更新用户名 | +| `/api/user/email` | PUT | 更新邮箱 | +| `/api/user/avatar` | PUT | 更新头像 | +| `/api/user/stats/:userId` | GET | 用户统计 | +| `/api/user/schemes/:userId` | GET | 用户方案列表 | + +### 方案分享 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/api/schemes/` | GET | 烽火地带方案列表 | +| `/api/schemes/:id` | GET | 方案详情 | +| `/api/schemes/` | POST | 创建方案 | +| `/api/schemes/:id` | DELETE | 删除方案 | +| `/api/schemes_aob/` | GET | 全面战场方案列表 | +| `/api/schemes_aob/:id` | GET | 方案详情 | +| `/api/schemes_aob/` | POST | 创建方案 | + +### 滤镜分享 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/api/filter-shares/` | GET | 滤镜列表 | +| `/api/filter-shares/:id` | GET | 滤镜详情 | +| `/api/filter-shares/` | POST | 创建滤镜 | +| `/api/filter-shares/:id` | DELETE | 删除滤镜 | + +### 收藏 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/api/favorites/` | GET | 收藏列表 | +| `/api/favorites/` | POST | 添加收藏 | +| `/api/favorites/:id` | DELETE | 取消收藏 | +| `/api/favorites/check` | GET | 检查是否收藏 | + +## VIP 卡密管理 + +### 生成卡密 + +运行种子脚本会自动生成卡密: + +```bash +pnpm db:seed +``` + +生成的卡密格式: +- 月卡:`VIP30-XXXX-XXXX-XXXX-XXXX` +- 季卡:`VIP90-XXXX-XXXX-XXXX-XXXX` +- 年卡:`VIP365-XXXX-XXXX-XXXX-XXXX` + +### 查询卡密 + +```bash +npx prisma studio +``` + +打开 Prisma Studio 可视化管理数据库。 + +## 项目结构 + +``` +maqt-backend/ +├── prisma/ +│ ├── schema.prisma # 数据库模型 +│ └── seed.ts # 种子数据 +├── src/ +│ ├── index.ts # 主入口 +│ ├── middleware/ +│ │ └── auth.ts # 认证中间件 +│ ├── routes/ +│ │ ├── auth.ts # 认证路由 +│ │ ├── user.ts # 用户路由 +│ │ ├── schemes.ts # 烽火地带方案 +│ │ ├── schemesAob.ts# 全面战场方案 +│ │ ├── filters.ts # 滤镜分享 +│ │ ├── favorites.ts # 收藏 +│ │ └── vip.ts # VIP 激活 +│ └── utils/ +│ ├── jwt.ts # JWT 工具 +│ └── encryption.ts# 加密工具 +├── package.json +├── tsconfig.json +└── .env.example +``` + +## 注意事项 + +1. **生产环境**请务必修改 `.env` 中的密钥 +2. **数据库**推荐使用 PostgreSQL +3. **部署**建议配合 PM2 或 Docker diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7b7715f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1882 @@ +{ + "name": "maqt-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "maqt-backend", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "^5.15.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "express-rate-limit": "^7.3.1", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "uuid": "^9.0.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20.14.2", + "@types/uuid": "^9.0.8", + "prisma": "^5.15.0", + "tsx": "^4.15.4", + "typescript": "^5.4.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e3f962 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "maqt-backend", + "version": "1.0.0", + "description": "码枪堂后端 API", + "main": "dist/index.js", + "scripts": { + "dev": "tsx watch 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": "tsx prisma/seed.ts" + }, + "dependencies": { + "@prisma/client": "^5.15.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "express-rate-limit": "^7.3.1", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "uuid": "^9.0.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20.14.2", + "@types/uuid": "^9.0.8", + "prisma": "^5.15.0", + "tsx": "^4.15.4", + "typescript": "^5.4.5" + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..f065a2a --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,279 @@ +// 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 = "postgresql" + url = env("DATABASE_URL") +} + +// ============================================ +// 用户表 +// ============================================ +model User { + id String @id @default(uuid()) + username String @unique @db.VarChar(50) + email String @unique @db.VarChar(100) + passwordHash String @map("password_hash") @db.VarChar(255) + avatar String? @db.VarChar(255) + + // VIP 状态 + isVip Boolean @default(false) @map("is_vip") + vipLevel Int @default(0) @map("vip_level") + vipExpireAt DateTime? @map("vip_expire_at") + + // 统计 + schemesCount Int @default(0) @map("schemes_count") + favoritesCount Int @default(0) @map("favorites_count") + + // 设备绑定 + installId String? @map("install_id") @db.VarChar(64) + deviceHash String? @map("device_hash") @db.VarChar(64) + lastDeviceCheck DateTime? @map("last_device_check") + + // 元数据 + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + lastLoginAt DateTime? @map("last_login_at") + deletedAt DateTime? @map("deleted_at") + + // 关系 + schemes Scheme[] + schemesAob SchemeAob[] + filterShares FilterShare[] + favorites Favorite[] + likes Like[] + logs UserLog[] + usedVipCards VipCard[] @relation("UsedCards") + deviceBindings DeviceBinding[] + + @@map("users") +} + +// ============================================ +// VIP 卡密表 +// ============================================ +model VipCard { + id String @id @default(uuid()) + cardKey String @unique @map("card_key") @db.VarChar(50) + + cardType String @map("card_type") @db.VarChar(20) + days Int + + status String @default("UNUSED") @db.VarChar(20) + usedBy String? @map("used_by") + usedAt DateTime? @map("used_at") + + batchId String? @map("batch_id") + generatedAt DateTime @default(now()) @map("generated_at") + + originalPrice Decimal? @map("original_price") @db.Decimal(10, 2) + salePrice Decimal? @map("sale_price") @db.Decimal(10, 2) + + user User? @relation("UsedCards", fields: [usedBy], references: [id], onDelete: SetNull) + + @@map("vip_cards") +} + +// ============================================ +// 方案表 - 烽火地带 +// ============================================ +model Scheme { + id String @id @default(uuid()) + userId String @map("user_id") + + title String? @db.VarChar(100) + description String? @db.Text + weaponName String? @map("weapon_name") @db.VarChar(100) + category String? @db.VarChar(50) + + schemeContent String @map("scheme_content") @db.Text + contentEncrypted Boolean @default(true) @map("content_encrypted") + + price Int @default(0) + + viewsCount Int @default(0) @map("views_count") + downloadsCount Int @default(0) @map("downloads_count") + likesCount Int @default(0) @map("likes_count") + favoritesCount Int @default(0) @map("favorites_count") + + gpuModel String? @map("gpu_model") @db.VarChar(100) + driverVersion String? @map("driver_version") @db.VarChar(50) + appVersion String? @map("app_version") @db.VarChar(20) + + status String @default("DRAFT") @db.VarChar(20) + isOfficial Boolean @default(false) @map("is_official") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([weaponName]) + @@index([category]) + @@index([status]) + @@map("schemes") +} + +// ============================================ +// 方案表 - 全面战场 (AOB) +// ============================================ +model SchemeAob { + id String @id @default(uuid()) + userId String @map("user_id") + + title String? @db.VarChar(100) + description String? @db.Text + weaponName String? @map("weapon_name") @db.VarChar(100) + category String? @db.VarChar(50) + + schemeContent String @map("scheme_content") @db.Text + contentEncrypted Boolean @default(true) @map("content_encrypted") + + price Int @default(0) + + viewsCount Int @default(0) @map("views_count") + downloadsCount Int @default(0) @map("downloads_count") + likesCount Int @default(0) @map("likes_count") + favoritesCount Int @default(0) @map("favorites_count") + + status String @default("DRAFT") @db.VarChar(20) + isOfficial Boolean @default(false) @map("is_official") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([weaponName]) + @@map("schemes_aob") +} + +// ============================================ +// 滤镜分享表 +// ============================================ +model FilterShare { + id String @id @default(uuid()) + userId String @map("user_id") + + title String @db.VarChar(100) + description String? @db.Text + category String? @db.VarChar(50) + + filterContent String @map("filter_content") @db.Text + contentFormat String @default("MQTS1") @map("content_format") @db.VarChar(20) + + viewsCount Int @default(0) @map("views_count") + likesCount Int @default(0) @map("likes_count") + + status String @default("DRAFT") @db.VarChar(20) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@map("filter_shares") +} + +// ============================================ +// 收藏表 +// ============================================ +model Favorite { + id String @id @default(uuid()) + userId String @map("user_id") + targetType String @map("target_type") @db.VarChar(20) + targetId String @map("target_id") + + createdAt DateTime @default(now()) @map("created_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, targetType, targetId]) + @@index([userId]) + @@map("favorites") +} + +// ============================================ +// 点赞表 +// ============================================ +model Like { + id String @id @default(uuid()) + userId String @map("user_id") + targetType String @map("target_type") @db.VarChar(20) + targetId String @map("target_id") + + createdAt DateTime @default(now()) @map("created_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, targetType, targetId]) + @@index([userId]) + @@index([targetType, targetId]) + @@map("likes") +} + +// ============================================ +// 分类表 +// ============================================ +model Category { + id String @id @default(uuid()) + name String @db.VarChar(50) + type String @db.VarChar(20) + + sortOrder Int @default(0) @map("sort_order") + isActive Boolean @default(true) @map("is_active") + + createdAt DateTime @default(now()) @map("created_at") + + @@map("categories") +} + +// ============================================ +// 用户日志表 +// ============================================ +model UserLog { + id String @id @default(uuid()) + userId String? @map("user_id") + + action String @db.VarChar(50) + targetType String? @map("target_type") @db.VarChar(50) + targetId String? @map("target_id") + + installId String? @map("install_id") @db.VarChar(64) + deviceHash String? @map("device_hash") @db.VarChar(64) + ipAddress String? @map("ip_address") @db.VarChar(45) + + createdAt DateTime @default(now()) @map("created_at") + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + + @@index([userId]) + @@index([action]) + @@map("user_logs") +} + +// ============================================ +// 设备绑定表 +// ============================================ +model DeviceBinding { + id String @id @default(uuid()) + userId String @map("user_id") + installId String @map("install_id") @db.VarChar(64) + deviceHash String @map("device_hash") @db.VarChar(64) + + boundAt DateTime @default(now()) @map("bound_at") + lastActiveAt DateTime? @map("last_active_at") + isActive Boolean @default(true) @map("is_active") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, installId]) + @@map("device_bindings") +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..5cd6cea --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,90 @@ +import { PrismaClient } from '@prisma/client'; +import crypto from 'crypto'; + +const prisma = new PrismaClient(); + +/** + * 生成 VIP 卡密 + */ +function generateCardKey(days: number): 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('-')}`; +} + +async function main() { + console.log('🌱 开始种子数据初始化...'); + + // ============================================ + // 创建分类 + // ============================================ + const categories = [ + // 烽火地带 + { 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(() => {}); // 忽略重复错误 + } + console.log(`✅ 创建 ${categories.length} 个分类`); + + // ============================================ + // 生成 VIP 卡密 + // ============================================ + const cardConfigs = [ + { type: 'MONTH', days: 30, count: 100 }, + { type: 'QUARTER', days: 90, count: 50 }, + { type: 'YEAR', days: 365, count: 20 }, + ]; + + let totalCards = 0; + for (const config of cardConfigs) { + const cards = []; + for (let i = 0; i < config.count; i++) { + cards.push({ + cardKey: generateCardKey(config.days), + cardType: config.type, + days: config.days, + }); + } + + await prisma.vipCard.createMany({ data: cards, skipDuplicates: true }); + totalCards += config.count; + console.log(`✅ 生成 ${config.type} 卡密 ${config.count} 张`); + } + + console.log(`\n🎉 种子数据初始化完成!`); + console.log(` - 分类:${categories.length} 个`); + console.log(` - VIP 卡密:${totalCards} 张`); +} + +main() + .catch((e) => { + console.error('❌ 种子数据初始化失败:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..aa6fee0 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,212 @@ +import express, { Request, Response } from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import rateLimit from 'express-rate-limit'; +import dotenv from 'dotenv'; + +dotenv.config(); + +import authRoutes from './routes/auth'; +import userRoutes from './routes/user'; +import schemeRoutes from './routes/schemes'; +import schemeAobRoutes from './routes/schemesAob'; +import filterRoutes from './routes/filters'; +import vipRoutes from './routes/vip'; +import favoriteRoutes from './routes/favorites'; + +const app = express(); +const PORT = process.env.PORT || 3001; + +// ============================================ +// 中间件 +// ============================================ + +// 安全头 - 开发模式禁用 CSP +app.use(helmet({ + contentSecurityPolicy: false, + crossOriginEmbedderPolicy: false, +})); + +// CORS +app.use(cors({ + origin: true, // 允许所有来源(开发模式) + credentials: true, +})); + +// JSON 解析 +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); + +// 速率限制 +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 分钟 + max: 100, // 每个 IP 最多 100 次请求 + message: { success: false, message: '请求过于频繁,请稍后再试' }, +}); +app.use('/api/', limiter); + +// 登录接口更严格的限制 +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, + message: { success: false, message: '登录尝试过于频繁,请稍后再试' }, +}); +app.use('/api/login', loginLimiter); +app.use('/api/register', loginLimiter); + +// ============================================ +// 路由 +// ============================================ + +app.get('/api/activity/ping', (req, res) => { + res.json({ success: true, message: 'pong', timestamp: Date.now() }); +}); + +app.use('/api', authRoutes); +app.use('/api/user', userRoutes); +app.use('/api/schemes', schemeRoutes); +app.use('/api/schemes_aob', schemeAobRoutes); +app.use('/api/filter-shares', filterRoutes); +app.use('/api', vipRoutes); +// 收藏计数(放在 favorites 路由之前,避免 auth 中间件拦截) +app.get('/api/favorites/count', async (req: Request, res: Response) => { + try { + const { PrismaClient } = require('@prisma/client'); + const prisma = new PrismaClient(); + const count = await prisma.favorite.count(); + res.json({ success: true, count }); + } catch (e) { + res.json({ success: true, count: 0 }); + } +}); + +app.use('/api/favorites', favoriteRoutes); + +// ============================================ +// 缺失的存根接口(防止前端 404) +// ============================================ + +// 二维码 +app.get('/api/qrcode/public/current', (req, res) => { + res.json({ success: true, data: { qrcode: null, url: '' } }); +}); + +// 弹窗 +app.get('/api/popups/', (req, res) => { + res.json({ success: true, data: [] }); +}); + +// 售后教程弹窗 +app.get('/api/aftersale-tutorial-popup', (req, res) => { + res.json({ success: true, data: { shown: false } }); +}); + +// 软件版本广告(空数据,dock 无广告项) +app.get('/api/software-version-ad', (req, res) => { + res.json({ success: true, data: { items: [] } }); +}); + +// 游戏地图密码缓存 +app.get('/api/game/map-password/cached', (req, res) => { + res.json({ success: true, data: [] }); +}); + +// 筛选分类 +app.get('/api/filter-share/categories', (req, res) => { + res.json({ success: true, data: [] }); +}); + +// ============================================ +// 武器调谐窗 (weapon-tuner) 所需的 API +// ============================================ + +// 武器分类列表 +app.get('/api/weapon-categories', (req, res) => { + const categories = [ + { category: 'AR', name: '突击步枪' }, + { category: 'SMG', name: '冲锋枪' }, + { category: 'SR', name: '狙击步枪' }, + { category: 'LMG', name: '轻机枪' }, + { category: 'SG', name: '霰弹枪' }, + { category: 'Pistol', name: '手枪' }, + { category: 'Launcher', name: '发射器' }, + ]; + res.json({ success: true, data: categories }); +}); + +// 武器列表(按分类筛选) +app.get('/api/weapons', (req, res) => { + res.json({ success: true, data: [] }); +}); + +// 分类下的武器 +app.get('/api/category/:code', (req, res) => { + res.json({ success: true, data: [] }); +}); + +// 广告列表 +app.get('/api/adverts/list', (req, res) => { + res.json({ success: true, data: [ + { + id: 1, + title: '码枪堂2.0 新版发布', + description: '全新界面,更多功能,一键优化三角洲行动游戏体验!', + author: '码枪堂官方', + avatar: null, + image_url: '', + link_url: 'https://wwamt.lanzout.com/b00odpq4wb', + isAdvert: true, + isVip: true, + shareTime: new Date().toISOString(), + } + ] }); +}); + +// 广告点击 +app.post('/api/adverts/:id/click', (req, res) => { + res.json({ success: true, message: 'ok' }); +}); + +// 头像列表 +app.get('/api/avatars', (req, res) => { + res.json({ success: true, data: [] }); +}); + +// ============================================ +// 错误处理 +// ============================================ + +app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error('Error:', err); + + if (err.name === 'UnauthorizedError') { + return res.status(401).json({ success: false, message: '访问令牌无效', code: 'INVALID_TOKEN' }); + } + + if (err.name === 'ZodError') { + return res.status(400).json({ success: false, message: '参数验证失败', errors: err.errors }); + } + + res.status(500).json({ success: false, message: '服务器内部错误' }); +}); + +// 404 +app.use((req, res) => { + res.status(404).json({ success: false, message: '接口不存在' }); +}); + +// ============================================ +// 启动 +// ============================================ + +app.listen(PORT, () => { + console.log(`🚀 码枪堂 API 运行在 http://localhost:${PORT}`); + console.log(`📋 可用接口:`); + console.log(` POST /api/login`); + console.log(` POST /api/register`); + console.log(` POST /api/activate-vip`); + console.log(` GET /api/session-status`); + console.log(` GET /api/vip-status`); +}); + +export default app; \ No newline at end of file diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 0000000..a6f64c0 --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,114 @@ +import { Request, Response, NextFunction } from 'express'; +import { verifyToken, extractToken } from '../utils/jwt'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +// 扩展 Request 类型 +declare global { + namespace Express { + interface Request { + user?: { + userId: string; + username: string; + email: string; + isVip: boolean; + vipLevel: number; + }; + } + } +} + +/** + * 认证中间件 + */ +export async function authMiddleware(req: Request, res: Response, next: NextFunction) { + try { + const token = extractToken(req.headers.authorization); + + if (!token) { + return res.status(401).json({ + success: false, + message: '访问令牌缺失,请先登录', + code: 'NO_TOKEN', + }); + } + + const payload = verifyToken(token); + + if (!payload) { + return res.status(401).json({ + success: false, + message: '访问令牌无效或已过期', + code: 'INVALID_TOKEN', + }); + } + + // 检查用户是否存在且未被删除 + const user = await prisma.user.findUnique({ + where: { id: payload.userId }, + select: { id: true, deletedAt: true }, + }); + + if (!user || user.deletedAt) { + return res.status(401).json({ + success: false, + message: '用户不存在或已被禁用', + code: 'USER_NOT_FOUND', + }); + } + + // 将用户信息附加到请求对象 + req.user = payload; + next(); + } catch (error) { + console.error('Auth middleware error:', error); + return res.status(500).json({ + success: false, + message: '认证服务异常', + }); + } +} + +/** + * 可选认证中间件(不强制要求登录) + */ +export async function optionalAuth(req: Request, res: Response, next: NextFunction) { + try { + const token = extractToken(req.headers.authorization); + + if (token) { + const payload = verifyToken(token); + if (payload) { + req.user = payload; + } + } + + next(); + } catch (error) { + next(); + } +} + +/** + * VIP 认证中间件(要求 VIP 用户) + */ +export async function vipMiddleware(req: Request, res: Response, next: NextFunction) { + if (!req.user) { + return res.status(401).json({ + success: false, + message: '请先登录', + code: 'NO_TOKEN', + }); + } + + if (!req.user.isVip) { + return res.status(403).json({ + success: false, + message: '此功能仅限 VIP 用户使用', + code: 'VIP_REQUIRED', + }); + } + + next(); +} diff --git a/src/routes/auth.ts b/src/routes/auth.ts new file mode 100644 index 0000000..328fc63 --- /dev/null +++ b/src/routes/auth.ts @@ -0,0 +1,269 @@ +import { Router, Request, Response } from 'express'; +import bcrypt from 'bcryptjs'; +import { PrismaClient } from '@prisma/client'; +import { z } from 'zod'; +import { generateToken } from '../utils/jwt'; +import { authMiddleware } from '../middleware/auth'; + +const router = Router(); +const prisma = new PrismaClient(); + +// ============================================ +// 注册 +// ============================================ +const registerSchema = z.object({ + username: z.string().min(3).max(50), + password: z.string().min(6).max(100), + email: z.string().email(), + installId: z.string().optional(), + deviceHash: z.string().optional(), +}); + +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: { + username: body.username, + email: body.email, + passwordHash, + installId: body.installId, + deviceHash: body.deviceHash, + }, + }); + + // 生成 Token + const token = generateToken({ + userId: user.id, + username: user.username, + email: user.email, + isVip: user.isVip, + vipLevel: user.vipLevel, + }); + + // 记录日志 + await prisma.userLog.create({ + data: { + userId: user.id, + action: 'REGISTER', + installId: body.installId, + deviceHash: body.deviceHash, + ipAddress: req.ip, + }, + }); + + res.json({ + success: true, + token, + user: { + id: user.id, + username: user.username, + email: user.email, + avatar: user.avatar, + isVip: user.isVip, + vipExpireAt: user.vipExpireAt, + vipLevel: user.vipLevel, + }, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + message: '参数验证失败', + errors: error.errors, + }); + } + + console.error('Register error:', error); + res.status(500).json({ success: false, message: '注册失败' }); + } +}); + +// ============================================ +// 登录 +// ============================================ +const loginSchema = z.object({ + username: z.string(), // 可以是用户名或邮箱 + password: z.string(), + installId: z.string().optional(), + deviceHash: z.string().optional(), + platform: z.string().optional(), + osVersion: z.string().optional(), + appVersion: z.string().optional(), +}); + +router.post('/login', async (req: Request, res: Response) => { + try { + const body = loginSchema.parse(req.body); + + // 查找用户(支持用户名或邮箱登录) + const user = await prisma.user.findFirst({ + where: { + OR: [ + { username: body.username }, + { email: body.username }, + ], + deletedAt: null, + }, + }); + + if (!user) { + return res.status(401).json({ + success: false, + message: '用户名或密码错误', + }); + } + + // 验证密码 + const validPassword = await bcrypt.compare(body.password, user.passwordHash); + + if (!validPassword) { + return res.status(401).json({ + success: false, + message: '用户名或密码错误', + }); + } + + // 更新最后登录时间和设备信息 + await prisma.user.update({ + where: { id: user.id }, + data: { + lastLoginAt: new Date(), + installId: body.installId || user.installId, + deviceHash: body.deviceHash || user.deviceHash, + }, + }); + + // 生成 Token + const token = generateToken({ + userId: user.id, + username: user.username, + email: user.email, + isVip: user.isVip, + vipLevel: user.vipLevel, + }); + + // 记录日志 + await prisma.userLog.create({ + data: { + userId: user.id, + action: 'LOGIN', + installId: body.installId, + deviceHash: body.deviceHash, + ipAddress: req.ip, + }, + }); + + res.json({ + success: true, + token, + user: { + id: user.id, + username: user.username, + email: user.email, + avatar: user.avatar, + isVip: user.isVip, + vipExpireAt: user.vipExpireAt, + vipLevel: user.vipLevel, + }, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + message: '参数验证失败', + errors: error.errors, + }); + } + + console.error('Login error:', error); + res.status(500).json({ success: false, message: '登录失败' }); + } +}); + +// ============================================ +// 会话状态 +// ============================================ +router.get('/session-status', authMiddleware, async (req: Request, res: Response) => { + res.json({ + success: true, + valid: true, + user: req.user, + }); +}); + +// ============================================ +// VIP 状态 +// ============================================ +router.get('/vip-status', authMiddleware, async (req: Request, res: Response) => { + try { + const user = await prisma.user.findUnique({ + where: { id: req.user!.userId }, + select: { isVip: true, vipExpireAt: true, vipLevel: true }, + }); + + if (!user) { + return res.status(404).json({ + success: false, + message: '用户不存在', + }); + } + + // 检查 VIP 是否过期 + let isVip = user.isVip; + if (user.vipExpireAt && new Date() > user.vipExpireAt) { + isVip = false; + // 更新数据库 + await prisma.user.update({ + where: { id: req.user!.userId }, + data: { isVip: false }, + }); + } + + const daysRemaining = user.vipExpireAt + ? Math.max(0, Math.ceil((user.vipExpireAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24))) + : 0; + + res.json({ + success: true, + isVip, + vipExpireAt: user.vipExpireAt, + vipLevel: user.vipLevel, + daysRemaining, + }); + } catch (error) { + console.error('VIP status error:', error); + res.status(500).json({ success: false, message: '获取 VIP 状态失败' }); + } +}); + +export default router; diff --git a/src/routes/favorites.ts b/src/routes/favorites.ts new file mode 100644 index 0000000..729c784 --- /dev/null +++ b/src/routes/favorites.ts @@ -0,0 +1,167 @@ +import { Router, Request, Response } from 'express'; +import { PrismaClient } from '@prisma/client'; +import { z } from 'zod'; +import { authMiddleware } from '../middleware/auth'; + +const router = Router(); +const prisma = new PrismaClient(); + +router.use(authMiddleware); + +const TARGET_TYPES = ['SCHEME', 'SCHEME_AOB', 'FILTER'] as const; + +// ============================================ +// 获取收藏列表 +// ============================================ +router.get('/', async (req: Request, res: Response) => { + try { + const { type, page = 1, limit = 20 } = req.query; + const skip = (Number(page) - 1) * Number(limit); + + const where: any = { userId: req.user!.userId }; + if (type && TARGET_TYPES.includes(type as any)) { + where.targetType = type; + } + + const favorites = await prisma.favorite.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip, + take: Number(limit), + }); + + res.json({ success: true, data: favorites }); + } catch (error) { + console.error('Get favorites error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +// ============================================ +// 添加收藏 +// ============================================ +const addFavoriteSchema = z.object({ + targetType: z.enum(TARGET_TYPES), + targetId: z.string().uuid(), +}); + +router.post('/', async (req: Request, res: Response) => { + try { + const body = addFavoriteSchema.parse(req.body); + + // 检查是否已收藏 + const existing = await prisma.favorite.findFirst({ + where: { + userId: req.user!.userId, + targetType: body.targetType, + targetId: body.targetId, + }, + }); + + if (existing) { + return res.status(400).json({ success: false, message: '已收藏' }); + } + + // 验证目标是否存在 + let targetExists = false; + if (body.targetType === 'SCHEME') { + targetExists = !!(await prisma.scheme.findFirst({ + where: { id: body.targetId, status: 'PUBLISHED' }, + })); + } else if (body.targetType === 'SCHEME_AOB') { + targetExists = !!(await prisma.schemeAob.findFirst({ + where: { id: body.targetId, status: 'PUBLISHED' }, + })); + } else if (body.targetType === 'FILTER') { + targetExists = !!(await prisma.filterShare.findFirst({ + where: { id: body.targetId, status: 'PUBLISHED' }, + })); + } + + if (!targetExists) { + return res.status(404).json({ success: false, message: '目标不存在' }); + } + + // 创建收藏 + await prisma.favorite.create({ + data: { + userId: req.user!.userId, + targetType: body.targetType, + targetId: body.targetId, + }, + }); + + // 更新用户收藏数 + await prisma.user.update({ + where: { id: req.user!.userId }, + data: { favoritesCount: { increment: 1 } }, + }); + + res.json({ success: true, message: '收藏成功' }); + } catch (error) { + console.error('Add favorite error:', error); + res.status(500).json({ success: false, message: '收藏失败' }); + } +}); + +// ============================================ +// 取消收藏 +// ============================================ +router.delete('/:id', async (req: Request, res: Response) => { + try { + const { id } = req.params; + + const favorite = await prisma.favorite.findUnique({ + where: { id }, + }); + + if (!favorite || favorite.userId !== req.user!.userId) { + return res.status(404).json({ success: false, message: '收藏不存在' }); + } + + await prisma.favorite.delete({ where: { id } }); + + // 更新用户收藏数 + await prisma.user.update({ + where: { id: req.user!.userId }, + data: { favoritesCount: { decrement: 1 } }, + }); + + res.json({ success: true, message: '已取消收藏' }); + } catch (error) { + console.error('Remove favorite error:', error); + res.status(500).json({ success: false, message: '取消收藏失败' }); + } +}); + +// ============================================ +// 检查是否已收藏 +// ============================================ +router.get('/check', async (req: Request, res: Response) => { + try { + const { targetType, targetId } = req.query; + + if (!targetType || !targetId) { + return res.status(400).json({ success: false, message: '参数缺失' }); + } + + const favorite = await prisma.favorite.findFirst({ + where: { + userId: req.user!.userId, + targetType: String(targetType), + targetId: String(targetId), + }, + }); + + res.json({ + success: true, + isFavorited: !!favorite, + favoriteId: favorite?.id || null, + }); + } catch (error) { + console.error('Check favorite error:', error); + res.status(500).json({ success: false, message: '检查失败' }); + } +}); + +export default router; \ No newline at end of file diff --git a/src/routes/filters.ts b/src/routes/filters.ts new file mode 100644 index 0000000..fca6026 --- /dev/null +++ b/src/routes/filters.ts @@ -0,0 +1,135 @@ +import { Router, Request, Response } from 'express'; +import { PrismaClient } from '@prisma/client'; +import { authMiddleware, optionalAuth } from '../middleware/auth'; + +const router = Router(); +const prisma = new PrismaClient(); + +// ============================================ +// 获取滤镜列表 +// ============================================ +router.get('/', optionalAuth, async (req: Request, res: Response) => { + try { + const { page = 1, limit = 20, category } = req.query; + const skip = (Number(page) - 1) * Number(limit); + + const where: any = { status: 'PUBLISHED' }; + if (category) where.category = String(category); + + const filters = await prisma.filterShare.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip, + take: Number(limit), + select: { + id: true, + title: true, + description: true, + category: true, + viewsCount: true, + likesCount: true, + createdAt: true, + user: { select: { id: true, username: true, avatar: true } }, + }, + }); + + res.json({ success: true, data: filters }); + } catch (error) { + console.error('Get filter_shares error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +// ============================================ +// 获取单个滤镜详情 +// ============================================ +router.get('/:id', optionalAuth, async (req: Request, res: Response) => { + try { + const { id } = req.params; + + const filter = await prisma.filterShare.findUnique({ + where: { id }, + include: { + user: { select: { id: true, username: true, avatar: true } }, + }, + }); + + if (!filter || (filter.status !== 'PUBLISHED' && filter.userId !== req.user?.userId)) { + return res.status(404).json({ success: false, message: '滤镜不存在' }); + } + + await prisma.filterShare.update({ + where: { id }, + data: { viewsCount: { increment: 1 } }, + }); + + res.json({ success: true, data: filter }); + } catch (error) { + console.error('Get filter_share error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +// ============================================ +// 创建滤镜 +// ============================================ +router.post('/', authMiddleware, async (req: Request, res: Response) => { + try { + const { title, description, category, filterContent, contentFormat = 'MQTS1' } = req.body; + + if (!title || !filterContent) { + return res.status(400).json({ success: false, message: '标题和内容不能为空' }); + } + + const filter = await prisma.filterShare.create({ + data: { + userId: req.user!.userId, + title, + description, + category, + filterContent, + contentFormat, + status: 'PUBLISHED', + }, + }); + + res.json({ success: true, message: '滤镜创建成功', data: filter }); + } catch (error) { + console.error('Create filter_share error:', error); + res.status(500).json({ success: false, message: '创建失败' }); + } +}); + +// ============================================ +// 删除滤镜 +// ============================================ +router.delete('/:id', authMiddleware, async (req: Request, res: Response) => { + try { + const { id } = req.params; + + const filter = await prisma.filterShare.findUnique({ + where: { id }, + select: { userId: true }, + }); + + if (!filter) { + return res.status(404).json({ success: false, message: '滤镜不存在' }); + } + + if (filter.userId !== req.user!.userId) { + return res.status(403).json({ success: false, message: '无权删除此滤镜' }); + } + + await prisma.filterShare.update({ + where: { id }, + data: { status: 'DELETED' }, + }); + + res.json({ success: true, message: '滤镜已删除' }); + } catch (error) { + console.error('Delete filter_share error:', error); + res.status(500).json({ success: false, message: '删除失败' }); + } +}); + +export default router; \ No newline at end of file diff --git a/src/routes/schemes.ts b/src/routes/schemes.ts new file mode 100644 index 0000000..4243b3e --- /dev/null +++ b/src/routes/schemes.ts @@ -0,0 +1,278 @@ +import { Router, Request, Response } from 'express'; +import { PrismaClient } from '@prisma/client'; +import { z } from 'zod'; +import { authMiddleware, optionalAuth } from '../middleware/auth'; +import { encrypt } from '../utils/encryption'; + +const router = Router(); +const prisma = new PrismaClient(); + +// ============================================ +// 获取方案列表 +// ============================================ +router.get('/', optionalAuth, async (req: Request, res: Response) => { + try { + const { + page = 1, + limit = 20, + weapon, + category, + sort = 'newest', + } = req.query; + + const skip = (Number(page) - 1) * Number(limit); + + const where: any = { status: 'PUBLISHED' }; + if (weapon) where.weaponName = { contains: String(weapon) }; + if (category) where.category = String(category); + + let orderBy: any = { createdAt: 'desc' }; + if (sort === 'popular') orderBy = { viewsCount: 'desc' }; + if (sort === 'downloads') orderBy = { downloadsCount: 'desc' }; + + const schemes = await prisma.scheme.findMany({ + where, + orderBy, + skip, + take: Number(limit), + select: { + id: true, + title: true, + description: true, + weaponName: true, + category: true, + price: true, + viewsCount: true, + downloadsCount: true, + likesCount: true, + isOfficial: true, + createdAt: true, + user: { + select: { + id: true, + username: true, + avatar: true, + }, + }, + }, + }); + + res.json({ + success: true, + data: schemes, + }); + } catch (error) { + console.error('Get schemes error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +// ============================================ +// 获取单个方案详情 +// ============================================ +router.get('/:id', optionalAuth, async (req: Request, res: Response) => { + try { + const { id } = req.params; + + const scheme = await prisma.scheme.findUnique({ + where: { id }, + include: { + user: { + select: { + id: true, + username: true, + avatar: true, + }, + }, + }, + }); + + if (!scheme) { + return res.status(404).json({ + success: false, + message: '方案不存在', + }); + } + + if (scheme.status !== 'PUBLISHED' && scheme.userId !== req.user?.userId) { + return res.status(404).json({ + success: false, + message: '方案不存在', + }); + } + + // 增加浏览量 + await prisma.scheme.update({ + where: { id }, + data: { viewsCount: { increment: 1 } }, + }); + + // 检查是否收藏 + let isFavorited = false; + if (req.user) { + const fav = await prisma.favorite.findFirst({ + where: { + userId: req.user.userId, + targetType: 'SCHEME', + targetId: id, + }, + }); + isFavorited = !!fav; + } + + // 加密方案内容 + const encryptedContent = encrypt(scheme.schemeContent); + + res.json({ + success: true, + data: { + ...scheme, + schemeContent: encryptedContent, + isFavorited, + }, + }); + } catch (error) { + console.error('Get scheme error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +// ============================================ +// 创建方案 +// ============================================ +const createSchemeSchema = z.object({ + title: z.string().min(1).max(100), + description: z.string().optional(), + weaponName: z.string().optional(), + category: z.string().optional(), + schemeContent: z.string().min(1), + price: z.number().int().min(0).default(0), + gpuModel: z.string().optional(), + driverVersion: z.string().optional(), + appVersion: z.string().optional(), +}); + +router.post('/', authMiddleware, async (req: Request, res: Response) => { + try { + const body = createSchemeSchema.parse(req.body); + + const scheme = await prisma.scheme.create({ + data: { + userId: req.user!.userId, + title: body.title, + description: body.description, + weaponName: body.weaponName, + category: body.category, + schemeContent: body.schemeContent, + price: body.price, + gpuModel: body.gpuModel, + driverVersion: body.driverVersion, + appVersion: body.appVersion, + status: 'PUBLISHED', + }, + }); + + // 更新用户方案数 + await prisma.user.update({ + where: { id: req.user!.userId }, + data: { schemesCount: { increment: 1 } }, + }); + + res.json({ + success: true, + message: '方案创建成功', + data: scheme, + }); + } catch (error) { + console.error('Create scheme error:', error); + res.status(500).json({ success: false, message: '创建失败' }); + } +}); + +// ============================================ +// 删除方案 +// ============================================ +router.delete('/:id', authMiddleware, async (req: Request, res: Response) => { + try { + const { id } = req.params; + + const scheme = await prisma.scheme.findUnique({ + where: { id }, + select: { userId: true }, + }); + + if (!scheme) { + return res.status(404).json({ + success: false, + message: '方案不存在', + }); + } + + if (scheme.userId !== req.user!.userId) { + return res.status(403).json({ + success: false, + message: '无权删除此方案', + }); + } + + await prisma.scheme.update({ + where: { id }, + data: { status: 'DELETED' }, + }); + + res.json({ + success: true, + message: '方案已删除', + }); + } catch (error) { + console.error('Delete scheme error:', error); + res.status(500).json({ success: false, message: '删除失败' }); + } +}); + +// 记录方案使用(增加下载计数) +router.post('/:id/use', authMiddleware, async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { source } = req.body; + + const scheme = await prisma.scheme.findUnique({ + where: { id }, + select: { id: true, downloadsCount: true }, + }); + + if (!scheme) { + return res.status(404).json({ + success: false, + message: '方案不存在', + }); + } + + // 使用计数 +1(增加 downloads 计数) + const updated = await prisma.scheme.update({ + where: { id }, + data: { downloadsCount: { increment: 1 } }, + select: { downloadsCount: true }, + }); + + // 记录日志 + await prisma.userLog.create({ + data: { + userId: req.user!.userId, + action: 'SchemeUse', + targetType: 'Scheme', + targetId: id, + }, + }); + + res.json({ + success: true, + downloadsCount: updated.downloadsCount, + }); + } catch (error) { + console.error('Scheme use error:', error); + res.status(500).json({ success: false, message: '记录失败' }); + } +}); + +export default router; diff --git a/src/routes/schemesAob.ts b/src/routes/schemesAob.ts new file mode 100644 index 0000000..5ca7618 --- /dev/null +++ b/src/routes/schemesAob.ts @@ -0,0 +1,122 @@ +import { Router, Request, Response } from 'express'; +import { PrismaClient } from '@prisma/client'; +import { authMiddleware, optionalAuth } from '../middleware/auth'; +import { encrypt } from '../utils/encryption'; + +const router = Router(); +const prisma = new PrismaClient(); + +// ============================================ +// 获取全面战场方案列表 +// ============================================ +router.get('/', optionalAuth, async (req: Request, res: Response) => { + try { + const { page = 1, limit = 20, weapon, category, sort = 'newest' } = req.query; + + const skip = (Number(page) - 1) * Number(limit); + + const where: any = { status: 'PUBLISHED' }; + if (weapon) where.weaponName = { contains: String(weapon) }; + if (category) where.category = String(category); + + let orderBy: any = { createdAt: 'desc' }; + if (sort === 'popular') orderBy = { viewsCount: 'desc' }; + if (sort === 'downloads') orderBy = { downloadsCount: 'desc' }; + + const schemes = await prisma.schemeAob.findMany({ + where, + orderBy, + skip, + take: Number(limit), + select: { + id: true, + title: true, + description: true, + weaponName: true, + category: true, + price: true, + viewsCount: true, + downloadsCount: true, + likesCount: true, + isOfficial: true, + createdAt: true, + user: { + select: { id: true, username: true, avatar: true }, + }, + }, + }); + + res.json({ success: true, data: schemes }); + } catch (error) { + console.error('Get schemes_aob error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +// ============================================ +// 获取单个方案详情 +// ============================================ +router.get('/:id', optionalAuth, async (req: Request, res: Response) => { + try { + const { id } = req.params; + + const scheme = await prisma.schemeAob.findUnique({ + where: { id }, + include: { + user: { select: { id: true, username: true, avatar: true } }, + }, + }); + + if (!scheme || (scheme.status !== 'PUBLISHED' && scheme.userId !== req.user?.userId)) { + return res.status(404).json({ success: false, message: '方案不存在' }); + } + + await prisma.schemeAob.update({ + where: { id }, + data: { viewsCount: { increment: 1 } }, + }); + + const encryptedContent = encrypt(scheme.schemeContent); + + res.json({ + success: true, + data: { ...scheme, schemeContent: encryptedContent }, + }); + } catch (error) { + console.error('Get scheme_aob error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +// ============================================ +// 创建方案 +// ============================================ +router.post('/', authMiddleware, async (req: Request, res: Response) => { + try { + const { title, description, weaponName, category, schemeContent, price = 0 } = req.body; + + if (!title || !schemeContent) { + return res.status(400).json({ success: false, message: '标题和内容不能为空' }); + } + + const scheme = await prisma.schemeAob.create({ + data: { + userId: req.user!.userId, + title, + description, + weaponName, + category, + schemeContent, + price, + status: 'PUBLISHED', + }, + }); + + res.json({ success: true, message: '方案创建成功', data: scheme }); + } catch (error) { + console.error('Create scheme_aob error:', error); + res.status(500).json({ success: false, message: '创建失败' }); + } +}); + +export default router; \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts new file mode 100644 index 0000000..1f43c15 --- /dev/null +++ b/src/routes/user.ts @@ -0,0 +1,337 @@ +import { Router, Request, Response } from 'express'; +import { PrismaClient } from '@prisma/client'; +import { z } from 'zod'; +import { authMiddleware } from '../middleware/auth'; + +const router = Router(); +const prisma = new PrismaClient(); + +// 所有路由都需要认证 +router.use(authMiddleware); + +// ============================================ +// 更新用户名 +// ============================================ +const updateUsernameSchema = z.object({ + userId: z.string().uuid(), + username: z.string().min(3).max(50), +}); + +router.put('/username', async (req: Request, res: Response) => { + try { + const body = updateUsernameSchema.parse(req.body); + + // 验证用户 ID + if (body.userId !== req.user!.userId) { + return res.status(403).json({ + success: false, + message: '无权修改其他用户信息', + }); + } + + // 检查用户名是否已存在 + const existing = await prisma.user.findUnique({ + where: { username: body.username }, + }); + + if (existing) { + return res.status(400).json({ + success: false, + message: '用户名已被使用', + }); + } + + // 检查修改次数限制 + const limit = await prisma.userLog.count({ + where: { + userId: req.user!.userId, + action: 'UPDATE_USERNAME', + }, + }); + + if (limit >= 4) { + return res.status(400).json({ + success: false, + message: '用户名修改次数已达上限', + }); + } + + // 更新用户名 + await prisma.user.update({ + where: { id: body.userId }, + data: { username: body.username }, + }); + + // 记录日志 + await prisma.userLog.create({ + data: { + userId: req.user!.userId, + action: 'UPDATE_USERNAME', + ipAddress: req.ip, + }, + }); + + res.json({ + success: true, + message: '用户名更新成功', + username: body.username, + }); + } catch (error) { + console.error('Update username error:', error); + res.status(500).json({ success: false, message: '更新失败' }); + } +}); + +// ============================================ +// 更新邮箱 +// ============================================ +const updateEmailSchema = z.object({ + userId: z.string().uuid(), + email: z.string().email(), +}); + +router.put('/email', async (req: Request, res: Response) => { + try { + const body = updateEmailSchema.parse(req.body); + + if (body.userId !== req.user!.userId) { + return res.status(403).json({ + success: false, + message: '无权修改其他用户信息', + }); + } + + // 检查邮箱是否已存在 + const existing = await prisma.user.findUnique({ + where: { email: body.email }, + }); + + if (existing) { + return res.status(400).json({ + success: false, + message: '邮箱已被使用', + }); + } + + await prisma.user.update({ + where: { id: body.userId }, + data: { email: body.email }, + }); + + res.json({ + success: true, + message: '邮箱更新成功', + email: body.email, + }); + } catch (error) { + console.error('Update email error:', error); + res.status(500).json({ success: false, message: '更新失败' }); + } +}); + +// ============================================ +// 更新头像 +// ============================================ +const updateAvatarSchema = z.object({ + userId: z.string().uuid(), + avatar: z.string().url(), +}); + +router.put('/avatar', async (req: Request, res: Response) => { + try { + const body = updateAvatarSchema.parse(req.body); + + if (body.userId !== req.user!.userId) { + return res.status(403).json({ + success: false, + message: '无权修改其他用户信息', + }); + } + + await prisma.user.update({ + where: { id: body.userId }, + data: { avatar: body.avatar }, + }); + + res.json({ + success: true, + message: '头像更新成功', + avatar: body.avatar, + }); + } catch (error) { + console.error('Update avatar error:', error); + res.status(500).json({ success: false, message: '更新失败' }); + } +}); + +// ============================================ +// 获取用户统计 +// ============================================ +router.get('/stats/:userId', async (req: Request, res: Response) => { + try { + const { userId } = req.params; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + schemesCount: true, + favoritesCount: true, + isVip: true, + vipExpireAt: true, + createdAt: true, + }, + }); + + if (!user) { + return res.status(404).json({ + success: false, + message: '用户不存在', + }); + } + + res.json({ + success: true, + data: { + schemesCount: user.schemesCount, + favoritesCount: user.favoritesCount, + isVip: user.isVip, + vipExpireAt: user.vipExpireAt, + memberSince: user.createdAt, + }, + }); + } catch (error) { + console.error('Get user stats error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +// ============================================ +// 获取用户方案列表 +// ============================================ +router.get('/schemes/:userId', async (req: Request, res: Response) => { + try { + const { userId } = req.params; + const { type = 'schemes', page = 1, limit = 20 } = req.query; + + const skip = (Number(page) - 1) * Number(limit); + + let schemes; + if (type === 'aob') { + schemes = await prisma.schemeAob.findMany({ + where: { userId, status: 'PUBLISHED' }, + orderBy: { createdAt: 'desc' }, + skip, + take: Number(limit), + select: { + id: true, title: true, weaponName: true, category: true, + viewsCount: true, downloadsCount: true, likesCount: true, createdAt: true, + }, + }); + } else { + schemes = await prisma.scheme.findMany({ + where: { userId, status: 'PUBLISHED' }, + orderBy: { createdAt: 'desc' }, + skip, + take: Number(limit), + select: { + id: true, title: true, weaponName: true, category: true, + viewsCount: true, downloadsCount: true, likesCount: true, createdAt: true, + }, + }); + } + + res.json({ + success: true, + data: schemes, + }); + } catch (error) { + console.error('Get user schemes error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +// ============================================ +// 获取用户被收藏次数 +// ============================================ +router.get('/favorited-count/:userId', async (req: Request, res: Response) => { + try { + const { userId } = req.params; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { favoritesCount: true }, + }); + + if (!user) { + return res.status(404).json({ + success: false, + message: '用户不存在', + }); + } + + res.json({ + success: true, + favoritedCount: user.favoritesCount, + }); + } catch (error) { + console.error('Get favorited count error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +// ============================================ +// 获取用户功能限制信息 +// ============================================ +router.get('/limits/:userId', async (req: Request, res: Response) => { + try { + const { userId } = req.params; + + // 验证用户是否存在 + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + isVip: true, + vipExpireAt: true, + }, + }); + + if (!user) { + return res.status(404).json({ + success: false, + message: '用户不存在', + }); + } + + // 检查 VIP 是否过期 + const now = new Date(); + const isVipActive = user.isVip && user.vipExpireAt && user.vipExpireAt > now; + + // 根据 VIP 状态返回不同的限制 + const limits = isVipActive + ? { + maxDailyUses: 500, // VIP: 每日最多使用500次 + dailyUsesLeft: 500, // 剩余次数(简化处理,实际需要统计当日使用) + maxFavorites: 1000, // VIP: 最多收藏1000个 + maxSchemes: 200, // VIP: 最多创建200个方案 + canUploadIcc: true, // VIP: 可以上传 ICC + } + : { + maxDailyUses: 50, // 非VIP: 每日最多使用50次 + dailyUsesLeft: 50, // 剩余次数 + maxFavorites: 100, // 非VIP: 最多收藏100个 + maxSchemes: 50, // 非VIP: 最多创建50个方案 + canUploadIcc: false, // 非VIP: 不能上传 ICC + }; + + res.json({ + success: true, + ...limits, + }); + } catch (error) { + console.error('Get user limits error:', error); + res.status(500).json({ success: false, message: '获取失败' }); + } +}); + +export default router; diff --git a/src/routes/vip.ts b/src/routes/vip.ts new file mode 100644 index 0000000..a74174a --- /dev/null +++ b/src/routes/vip.ts @@ -0,0 +1,112 @@ +import { Router, Request, Response } from 'express'; +import { PrismaClient } from '@prisma/client'; +import { z } from 'zod'; +import { authMiddleware } from '../middleware/auth'; + +const router = Router(); +const prisma = new PrismaClient(); + +// ============================================ +// VIP 卡密激活 +// ============================================ +const activateSchema = z.object({ + cardKey: z.string().regex(/^VIP\d+-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/), +}); + +router.post('/activate-vip', authMiddleware, async (req: Request, res: Response) => { + try { + const body = activateSchema.parse(req.body); + + // 查找卡密 + const card = await prisma.vipCard.findUnique({ + where: { cardKey: body.cardKey }, + }); + + if (!card) { + return res.status(400).json({ + success: false, + message: '卡密不存在', + }); + } + + if (card.status !== 'UNUSED') { + return res.status(400).json({ + success: false, + message: '卡密已被使用或已失效', + }); + } + + // 获取用户当前 VIP 状态 + const user = await prisma.user.findUnique({ + where: { id: req.user!.userId }, + select: { isVip: true, vipExpireAt: true, vipLevel: true }, + }); + + if (!user) { + return res.status(404).json({ + success: false, + message: '用户不存在', + }); + } + + // 计算新的 VIP 过期时间 + let newExpireAt: Date; + if (user.vipExpireAt && user.isVip && new Date() < user.vipExpireAt) { + // 已是 VIP,延长时间 + newExpireAt = new Date(user.vipExpireAt.getTime() + card.days * 24 * 60 * 60 * 1000); + } else { + // 非 VIP,从现在开始计算 + newExpireAt = new Date(Date.now() + card.days * 24 * 60 * 60 * 1000); + } + + // 更新用户 VIP 状态 + const updatedUser = await prisma.user.update({ + where: { id: req.user!.userId }, + data: { + isVip: true, + vipExpireAt: newExpireAt, + vipLevel: Math.max(user.vipLevel, 1), + }, + }); + + // 更新卡密状态 + await prisma.vipCard.update({ + where: { id: card.id }, + data: { + status: 'USED', + usedBy: req.user!.userId, + usedAt: new Date(), + }, + }); + + // 记录日志 + await prisma.userLog.create({ + data: { + userId: req.user!.userId, + action: 'VIP_ACTIVATE', + targetId: card.id, + ipAddress: req.ip, + }, + }); + + res.json({ + success: true, + isVip: true, + vipExpireAt: newExpireAt, + vipLevel: updatedUser.vipLevel, + message: `激活成功,VIP 有效期延长 ${card.days} 天`, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + message: '卡密格式无效', + }); + } + + console.error('Activate VIP error:', error); + res.status(500).json({ success: false, message: '激活失败' }); + } +}); + +export default router; \ No newline at end of file diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts new file mode 100644 index 0000000..0ce4f13 --- /dev/null +++ b/src/utils/encryption.ts @@ -0,0 +1,64 @@ +import crypto from 'crypto'; + +const ALGORITHM = 'aes-256-cbc'; +const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef'; + +export interface EncryptedData { + encrypted: boolean; + iv: string; + data: string; +} + +/** + * AES 加密 + */ +export function encrypt(text: string): EncryptedData { + const iv = crypto.randomBytes(16); + const key = Buffer.from(ENCRYPTION_KEY, 'utf-8'); + + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + let encrypted = cipher.update(text, 'utf-8', 'hex'); + encrypted += cipher.final('hex'); + + return { + encrypted: true, + iv: iv.toString('hex'), + data: encrypted, + }; +} + +/** + * AES 解密 + */ +export function decrypt(ivHex: string, dataHex: string): string { + const iv = Buffer.from(ivHex, 'hex'); + const encryptedData = Buffer.from(dataHex, 'hex'); + const key = Buffer.from(ENCRYPTION_KEY, 'utf-8'); + + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + let decrypted = decipher.update(encryptedData, undefined, 'utf-8'); + decrypted += decipher.final('utf-8'); + + return decrypted; +} + +/** + * 密码哈希 + */ +export function hashPassword(password: string): string { + return crypto.createHash('sha256').update(password).digest('hex'); +} + +/** + * 生成随机字符串 + */ +export function randomString(length: number = 32): string { + return crypto.randomBytes(length).toString('hex').slice(0, length); +} + +/** + * 生成设备哈希 + */ +export function generateDeviceHash(data: string): string { + return crypto.createHash('sha256').update(data).digest('hex').slice(0, 64); +} diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 0000000..80751a6 --- /dev/null +++ b/src/utils/jwt.ts @@ -0,0 +1,47 @@ +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const jwtSign = jwt.sign as any; + +export interface JwtPayload { + userId: string; + username: string; + email: string; + isVip: boolean; + vipLevel: number; +} + +/** + * 生成 JWT Token + */ +export function generateToken(payload: JwtPayload): string { + return jwtSign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); +} + +/** + * 验证 JWT Token + */ +export function verifyToken(token: string): JwtPayload | null { + try { + return jwt.verify(token, JWT_SECRET) as JwtPayload; + } catch (error) { + return null; + } +} + +/** + * 从请求头提取 Token + */ +export function extractToken(authHeader?: string): string | null { + if (!authHeader) return null; + + const parts = authHeader.split(' '); + if (parts.length !== 2 || parts[0] !== 'Bearer') { + return null; + } + + return parts[1]; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9997121 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file