first commit
This commit is contained in:
parent
0072220d05
commit
3c62871948
17
.env.example
Normal file
17
.env.example
Normal file
@ -0,0 +1,17 @@
|
||||
# Database
|
||||
DATABASE_URL="postgresql://username:password@localhost:5432/winupdate_neo?schema=public"
|
||||
|
||||
# Auth
|
||||
AUTH_USERNAME=admin
|
||||
AUTH_PASSWORD=password
|
||||
|
||||
# Port
|
||||
PORT=3000
|
||||
|
||||
# MinIO Configuration
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_ACCESS_KEY=your_access_key
|
||||
MINIO_SECRET_KEY=your_secret_key
|
||||
MINIO_BUCKET_NAME=winupdate
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -32,6 +32,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@ -39,3 +40,5 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
/app/generated/prisma
|
||||
|
||||
247
DEPLOYMENT.md
Normal file
247
DEPLOYMENT.md
Normal file
@ -0,0 +1,247 @@
|
||||
# Winupdate Neo 部署指南
|
||||
|
||||
## 系统架构
|
||||
|
||||
本项目使用以下技术栈:
|
||||
|
||||
- **前端**: Next.js 15 (TypeScript)
|
||||
- **数据库**: PostgreSQL + Prisma ORM
|
||||
- **对象存储**: MinIO (自建 S3 兼容存储)
|
||||
- **包管理**: Bun
|
||||
- **运行时**: Node.js
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
#### 1.1 安装 Bun
|
||||
```bash
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
# 或在 Windows 上:
|
||||
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||
```
|
||||
|
||||
#### 1.2 安装 PostgreSQL
|
||||
确保 PostgreSQL 服务正在运行,并创建数据库。
|
||||
|
||||
#### 1.3 部署 MinIO
|
||||
```bash
|
||||
# Docker 方式部署 MinIO
|
||||
docker run -p 9000:9000 -p 9001:9001 \
|
||||
-v /data/minio:/data \
|
||||
-e "MINIO_ROOT_USER=feie9454" \
|
||||
-e "MINIO_ROOT_PASSWORD=zjh94544549ok" \
|
||||
minio/minio server /data --console-address ":9001"
|
||||
```
|
||||
|
||||
访问 http://192.168.5.13:9001 进入 MinIO 控制台,创建名为 "winupdate" 的 bucket。
|
||||
|
||||
### 2. 项目配置
|
||||
|
||||
#### 2.1 克隆项目
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd winupdate-neo
|
||||
```
|
||||
|
||||
#### 2.2 安装依赖
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
#### 2.3 配置环境变量
|
||||
复制并编辑 `.env` 文件:
|
||||
|
||||
```env
|
||||
# 数据库配置
|
||||
DATABASE_URL="postgresql://username:password@localhost:5432/winupdate_neo?schema=public"
|
||||
|
||||
# 认证配置
|
||||
AUTH_USERNAME=admin
|
||||
AUTH_PASSWORD=password
|
||||
|
||||
# 服务端口
|
||||
PORT=3000
|
||||
|
||||
# MinIO 配置(在代码中硬编码,生产环境建议移到环境变量)
|
||||
```
|
||||
|
||||
#### 2.4 初始化数据库
|
||||
```bash
|
||||
# 生成 Prisma 客户端
|
||||
bun run db:generate
|
||||
|
||||
# 运行数据库迁移
|
||||
bun run db:migrate
|
||||
```
|
||||
|
||||
### 3. 测试部署
|
||||
|
||||
#### 3.1 测试 MinIO 连接
|
||||
```bash
|
||||
bun run test-minio.ts
|
||||
```
|
||||
|
||||
应该看到:
|
||||
```
|
||||
🎉 MinIO 测试完全成功!可以开始使用了。
|
||||
```
|
||||
|
||||
#### 3.2 测试文件操作
|
||||
```bash
|
||||
bun run test-file-operations.ts
|
||||
```
|
||||
|
||||
应该看到完整的文件上传、下载、删除测试成功。
|
||||
|
||||
#### 3.3 启动开发服务器
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
访问 http://localhost:3000/api-test 查看 API 状态。
|
||||
|
||||
### 4. 生产部署
|
||||
|
||||
#### 4.1 构建项目
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
#### 4.2 启动生产服务器
|
||||
```bash
|
||||
bun run start
|
||||
```
|
||||
|
||||
## 存储架构说明
|
||||
|
||||
### MinIO 对象存储结构
|
||||
|
||||
```
|
||||
winupdate bucket/
|
||||
├── screenshots/ # 截图文件
|
||||
│ ├── 2025/
|
||||
│ │ ├── 01/
|
||||
│ │ │ ├── hostname1/
|
||||
│ │ │ │ ├── uuid1.webp
|
||||
│ │ │ │ └── uuid2.webp
|
||||
│ │ │ └── hostname2/
|
||||
│ │ └── 02/
|
||||
│ └── 2024/
|
||||
├── versions/ # 版本文件
|
||||
│ ├── 2025/
|
||||
│ │ ├── 06/
|
||||
│ │ │ ├── uuid1.exe
|
||||
│ │ │ └── uuid2.exe
|
||||
│ │ └── 07/
|
||||
│ └── 2024/
|
||||
└── files/ # 其他文件
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 性能优化特性
|
||||
|
||||
1. **分层目录结构**: 按时间和主机名分层,避免单目录文件过多
|
||||
2. **数据库索引**: objectName 字段建立索引,快速查找
|
||||
3. **元数据存储**: 文件元数据存储在 MinIO 中,避免额外数据库查询
|
||||
4. **缓存友好**: 静态文件设置长期缓存
|
||||
5. **并发支持**: MinIO 原生支持高并发读写
|
||||
|
||||
### 扩展能力
|
||||
|
||||
1. **水平扩展**: MinIO 支持分布式部署
|
||||
2. **存储容量**: 理论上无限制,实际受硬件限制
|
||||
3. **备份恢复**: 支持 S3 兼容的备份工具
|
||||
4. **监控告警**: 可集成 Prometheus + Grafana
|
||||
|
||||
## API 接口说明
|
||||
|
||||
### 文件上传流程
|
||||
|
||||
1. 客户端上传文件到 `/api/hosts/{hostname}/screenshots`
|
||||
2. 服务器将文件存储到 MinIO,生成 objectName
|
||||
3. 在数据库中保存文件元数据,包含 objectName
|
||||
4. 返回成功响应
|
||||
|
||||
### 文件下载流程
|
||||
|
||||
1. 客户端请求 `/api/screenshots/{fileId}`
|
||||
2. 服务器根据 fileId 从数据库查找 objectName
|
||||
3. 从 MinIO 下载文件
|
||||
4. 返回文件数据
|
||||
|
||||
## 监控和维护
|
||||
|
||||
### 日志监控
|
||||
- MinIO 操作日志
|
||||
- 应用程序日志
|
||||
- 数据库查询日志
|
||||
|
||||
### 存储监控
|
||||
```bash
|
||||
# 查看存储统计
|
||||
curl http://localhost:3000/api/storage/stats
|
||||
```
|
||||
|
||||
### 清理策略
|
||||
- 定期清理过期截图(可配置保留时间)
|
||||
- 监控存储空间使用情况
|
||||
- 设置存储告警阈值
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **MinIO 连接失败**
|
||||
- 检查 MinIO 服务状态
|
||||
- 确认网络连通性
|
||||
- 验证认证信息
|
||||
|
||||
2. **数据库连接失败**
|
||||
- 检查 PostgreSQL 服务状态
|
||||
- 确认连接字符串正确
|
||||
- 检查用户权限
|
||||
|
||||
3. **文件上传失败**
|
||||
- 检查磁盘空间
|
||||
- 确认 MinIO bucket 存在
|
||||
- 查看 MinIO 日志
|
||||
|
||||
4. **性能问题**
|
||||
- 监控数据库查询性能
|
||||
- 检查 MinIO 服务器负载
|
||||
- 优化数据库索引
|
||||
|
||||
### 性能调优建议
|
||||
|
||||
1. **数据库优化**
|
||||
- 定期更新统计信息
|
||||
- 监控慢查询
|
||||
- 适当增加连接池大小
|
||||
|
||||
2. **MinIO 优化**
|
||||
- 使用 SSD 存储
|
||||
- 配置适当的并发数
|
||||
- 启用压缩(如果需要)
|
||||
|
||||
3. **应用优化**
|
||||
- 启用文件缓存
|
||||
- 使用 CDN(如果有公网访问需求)
|
||||
- 实现文件预签名 URL(减少服务器负载)
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **访问控制**
|
||||
- 使用强密码
|
||||
- 定期轮换认证信息
|
||||
- 限制网络访问范围
|
||||
|
||||
2. **数据安全**
|
||||
- 启用数据加密
|
||||
- 定期备份数据
|
||||
- 监控异常访问
|
||||
|
||||
3. **网络安全**
|
||||
- 使用 HTTPS
|
||||
- 配置防火墙
|
||||
- 启用访问日志
|
||||
196
README.md
196
README.md
@ -1,36 +1,188 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# Winupdate Neo
|
||||
|
||||
## Getting Started
|
||||
这是使用 Next.js + Prisma + PostgreSQL 重写的 Winupdate 项目的 API 部分。
|
||||
|
||||
First, run the development server:
|
||||
## 技术栈
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
- **前端框架**: Next.js 15
|
||||
- **数据库**: PostgreSQL + Prisma ORM
|
||||
- **包管理器**: Bun
|
||||
- **文件存储**: 本地文件系统(替代 GridFS)
|
||||
- **语言**: TypeScript
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
app/
|
||||
├── api/ # API 路由
|
||||
│ ├── hosts/
|
||||
│ │ ├── route.ts # 主机列表 API
|
||||
│ │ └── [hostname]/
|
||||
│ │ ├── screenshots/
|
||||
│ │ │ └── route.ts # 截图上传/获取 API
|
||||
│ │ ├── credentials/
|
||||
│ │ │ └── route.ts # 凭据管理 API
|
||||
│ │ └── time-distribution/
|
||||
│ │ └── route.ts # 时间分布统计 API
|
||||
│ ├── screenshots/
|
||||
│ │ └── [fileId]/
|
||||
│ │ └── route.ts # 截图文件服务 API
|
||||
│ ├── downloads/
|
||||
│ │ └── [fileId]/
|
||||
│ │ └── route.ts # 文件下载 API
|
||||
│ ├── version/
|
||||
│ │ └── route.ts # 版本信息 API
|
||||
│ └── upload/
|
||||
│ └── version/
|
||||
│ └── route.ts # 版本上传 API
|
||||
├── api-test/ # API 测试页面
|
||||
└── ... # 其他 Next.js 文件
|
||||
|
||||
lib/
|
||||
├── prisma.ts # Prisma 客户端配置
|
||||
├── config.ts # 应用配置
|
||||
├── fileStorage.ts # 文件存储工具
|
||||
├── middleware.ts # 中间件工具
|
||||
└── push.ts # 推送通知工具
|
||||
|
||||
prisma/
|
||||
├── schema.prisma # 数据库模式
|
||||
└── migrations/ # 数据库迁移文件
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
## 快速开始
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
### 1. 安装依赖
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
## Learn More
|
||||
### 2. 配置环境变量
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
复制 `.env.example` 文件为 `.env` 并配置相应的设置:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
配置文件内容示例:
|
||||
|
||||
## Deploy on Vercel
|
||||
```env
|
||||
# Database
|
||||
DATABASE_URL="postgresql://username:password@localhost:5432/winupdate_neo?schema=public"
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
# Auth
|
||||
AUTH_USERNAME=admin
|
||||
AUTH_PASSWORD=password
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
# Port
|
||||
PORT=3000
|
||||
|
||||
# MinIO Configuration
|
||||
MINIO_ENDPOINT=192.168.5.13
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_ACCESS_KEY=your_access_key
|
||||
MINIO_SECRET_KEY=your_secret_key
|
||||
MINIO_BUCKET_NAME=winupdate
|
||||
```
|
||||
|
||||
### 3. 初始化数据库
|
||||
|
||||
```bash
|
||||
# 生成 Prisma 客户端
|
||||
bun run db:generate
|
||||
|
||||
# 运行数据库迁移
|
||||
bun run db:migrate
|
||||
```
|
||||
|
||||
### 4. 启动开发服务器
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## API 端点
|
||||
|
||||
### 主机管理
|
||||
|
||||
- `GET /api/hosts` - 获取主机列表
|
||||
- `POST /api/hosts/{hostname}/screenshots` - 上传截图
|
||||
- `GET /api/hosts/{hostname}/screenshots` - 获取截图记录
|
||||
- `GET /api/hosts/{hostname}/time-distribution` - 获取时间分布统计
|
||||
|
||||
### 凭据管理
|
||||
|
||||
- `POST /api/hosts/{hostname}/credentials` - 上传凭据
|
||||
- `GET /api/hosts/{hostname}/credentials` - 获取凭据
|
||||
|
||||
### 文件服务
|
||||
|
||||
- `GET /api/screenshots/{fileId}` - 获取截图文件
|
||||
- `GET /api/downloads/{fileId}` - 下载文件
|
||||
|
||||
### 版本管理
|
||||
|
||||
- `GET /api/version` - 获取最新版本信息
|
||||
- `POST /api/upload/version` - 上传新版本
|
||||
|
||||
## 数据库模型
|
||||
|
||||
### Host
|
||||
- 主机基本信息(hostname, lastUpdate)
|
||||
|
||||
### Record
|
||||
- 截图记录(timestamp, windows, screenshots)
|
||||
|
||||
### Window
|
||||
- 窗口信息(title, path, memory)
|
||||
|
||||
### Screenshot
|
||||
- 截图文件信息(fileId, filename, monitorName)
|
||||
|
||||
### Credential
|
||||
- 凭据信息(hostname, username, browser, url, login)
|
||||
|
||||
### Password
|
||||
- 密码历史(value, timestamp)
|
||||
|
||||
### Version
|
||||
- 版本信息(version, fileId, checksum, isLatest)
|
||||
|
||||
## 与原项目的区别
|
||||
|
||||
1. **数据库**: 从 MongoDB 迁移到 PostgreSQL
|
||||
2. **ORM**: 使用 Prisma 替代 Mongoose
|
||||
3. **文件存储**: 使用本地文件系统替代 GridFS
|
||||
4. **框架**: 从 Express 迁移到 Next.js API Routes
|
||||
5. **包管理**: 使用 Bun 替代 npm/yarn
|
||||
|
||||
## 开发工具
|
||||
|
||||
```bash
|
||||
# 数据库相关
|
||||
bun run db:generate # 生成 Prisma 客户端
|
||||
bun run db:migrate # 运行数据库迁移
|
||||
bun run db:reset # 重置数据库
|
||||
bun run db:studio # 打开 Prisma Studio
|
||||
|
||||
# 开发
|
||||
bun run dev # 启动开发服务器
|
||||
bun run build # 构建生产版本
|
||||
bun run start # 启动生产服务器
|
||||
bun run lint # 代码检查
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 文件存储在 MinIO 对象存储中,按分层结构组织
|
||||
2. 需要确保 PostgreSQL 数据库和 MinIO 服务正在运行
|
||||
3. 所有配置通过环境变量管理,包括数据库、认证和 MinIO 设置
|
||||
4. MinIO 需要预先创建对应的 bucket
|
||||
5. 推送通知功能目前只是控制台日志,可以根据需要集成真实的推送服务
|
||||
|
||||
## API 兼容性
|
||||
|
||||
这个重写版本保持了与原始 Express 应用相同的 API 接口,确保客户端代码无需修改即可使用。
|
||||
|
||||
74
app/api-test/page.tsx
Normal file
74
app/api-test/page.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
export default function ApiTest() {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Winupdate Neo API 测试</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="border p-4 rounded">
|
||||
<h2 className="text-lg font-semibold mb-2">API 端点</h2>
|
||||
<ul className="space-y-1 text-sm">
|
||||
<li>• GET /api/hosts - 获取主机列表</li>
|
||||
<li>• POST /api/hosts/[hostname]/screenshots - 上传截图</li>
|
||||
<li>• GET /api/hosts/[hostname]/screenshots - 获取截图记录</li>
|
||||
<li>• POST /api/hosts/[hostname]/credentials - 上传凭据</li>
|
||||
<li>• GET /api/hosts/[hostname]/credentials - 获取凭据</li>
|
||||
<li>• GET /api/hosts/[hostname]/time-distribution - 获取时间分布</li>
|
||||
<li>• GET /api/version - 获取最新版本</li>
|
||||
<li>• POST /api/upload/version - 上传新版本</li>
|
||||
<li>• GET /api/screenshots/[fileId] - 获取截图文件</li>
|
||||
<li>• GET /api/downloads/[fileId] - 下载文件</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border p-4 rounded">
|
||||
<h2 className="text-lg font-semibold mb-2">数据库模型</h2>
|
||||
<ul className="space-y-1 text-sm">
|
||||
<li>• Host - 主机信息</li>
|
||||
<li>• Record - 记录信息</li>
|
||||
<li>• Window - 窗口信息</li>
|
||||
<li>• Screenshot - 截图信息</li>
|
||||
<li>• Credential - 凭据信息</li>
|
||||
<li>• Password - 密码历史</li>
|
||||
<li>• Version - 版本信息</li>
|
||||
<li>• Nssm - NSSM 文件</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border p-4 rounded">
|
||||
<h2 className="text-lg font-semibold mb-2">环境变量</h2>
|
||||
<ul className="space-y-1 text-sm">
|
||||
<li>• DATABASE_URL - 数据库连接字符串</li>
|
||||
<li>• AUTH_USERNAME - 认证用户名</li>
|
||||
<li>• AUTH_PASSWORD - 认证密码</li>
|
||||
<li>• PORT - 服务端口</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border p-4 rounded">
|
||||
<h2 className="text-lg font-semibold mb-2">MinIO 对象存储</h2>
|
||||
<ul className="space-y-1 text-sm">
|
||||
<li>• 服务器: 192.168.5.13:9000</li>
|
||||
<li>• Bucket: winupdate</li>
|
||||
<li>• 存储结构: 按类型/年/月/日/主机名分层</li>
|
||||
<li>• 截图路径: screenshots/年/月/日/主机名/文件</li>
|
||||
<li>• 版本路径: versions/年/月/文件</li>
|
||||
<li>• 支持元数据存储和检索</li>
|
||||
<li>• 自动文件分布和负载均衡</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border p-4 rounded">
|
||||
<h2 className="text-lg font-semibold mb-2">性能优化特性</h2>
|
||||
<ul className="space-y-1 text-sm">
|
||||
<li>• 分层目录结构避免单目录文件过多</li>
|
||||
<li>• 数据库存储 objectName 避免搜索开销</li>
|
||||
<li>• 文件元数据存储在 MinIO 中</li>
|
||||
<li>• 支持并发上传下载</li>
|
||||
<li>• 缓存友好的文件访问</li>
|
||||
<li>• 自动压缩和去重</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
62
app/api/downloads/[fileId]/route.ts
Normal file
62
app/api/downloads/[fileId]/route.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getFileByObjectName } from '@/lib/fileStorage'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { withCors } from '@/lib/middleware'
|
||||
|
||||
async function handleDownload(req: NextRequest) {
|
||||
try {
|
||||
const pathSegments = req.nextUrl.pathname.split('/')
|
||||
const fileIdIndex = pathSegments.indexOf('downloads') + 1
|
||||
const fileId = pathSegments[fileIdIndex]
|
||||
|
||||
if (!fileId) {
|
||||
return NextResponse.json({ error: '缺少文件ID' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 从数据库查找 objectName
|
||||
let objectName: string | null = null
|
||||
|
||||
// 尝试从 Version 表查找
|
||||
const version = await prisma.version.findFirst({
|
||||
where: { fileId },
|
||||
select: { objectName: true }
|
||||
})
|
||||
|
||||
if (version) {
|
||||
objectName = version.objectName
|
||||
} else {
|
||||
// 尝试从 Nssm 表查找
|
||||
const nssm = await prisma.nssm.findFirst({
|
||||
where: { fileId },
|
||||
select: { objectName: true }
|
||||
})
|
||||
|
||||
if (nssm) {
|
||||
objectName = nssm.objectName
|
||||
}
|
||||
}
|
||||
|
||||
if (!objectName) {
|
||||
return NextResponse.json({ error: '文件不存在' }, { status: 404 })
|
||||
}
|
||||
|
||||
const file = await getFileByObjectName(objectName)
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: '文件不存在' }, { status: 404 })
|
||||
}
|
||||
|
||||
return new NextResponse(file.buffer, {
|
||||
headers: {
|
||||
'Content-Type': file.contentType || 'application/octet-stream',
|
||||
'Content-Disposition': `attachment; filename=${file.filename || 'download'}`,
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
return NextResponse.json({ error: '下载失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withCors(handleDownload)
|
||||
168
app/api/hosts/[hostname]/credentials/route.ts
Normal file
168
app/api/hosts/[hostname]/credentials/route.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { withCors } from '@/lib/middleware'
|
||||
|
||||
async function handleCredentialsPost(req: NextRequest) {
|
||||
try {
|
||||
const pathSegments = req.nextUrl.pathname.split('/')
|
||||
const hostnameIndex = pathSegments.indexOf('hosts') + 1
|
||||
const hostname = pathSegments[hostnameIndex]
|
||||
|
||||
if (!hostname) {
|
||||
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentialsData = await req.json()
|
||||
console.log(JSON.stringify(credentialsData))
|
||||
const currentTime = new Date()
|
||||
|
||||
if (!Array.isArray(credentialsData) || credentialsData.length < 1) {
|
||||
return NextResponse.json({ error: '无效的凭据数据格式' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Extract username from the first element
|
||||
const [userKey, username] = credentialsData[0]
|
||||
if (userKey !== 'User' || !username) {
|
||||
return NextResponse.json({ error: '无效的用户信息' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Process browser credentials
|
||||
const savedCredentials = []
|
||||
|
||||
for (let i = 1; i < credentialsData.length; i++) {
|
||||
const browserData = credentialsData[i]
|
||||
|
||||
if (!Array.isArray(browserData) || browserData.length < 3) {
|
||||
continue
|
||||
}
|
||||
|
||||
const [enabled, browserName, browserCredentials] = browserData
|
||||
|
||||
if (!enabled || !browserName || !Array.isArray(browserCredentials)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Process each credential for this browser
|
||||
for (const cred of browserCredentials) {
|
||||
const { URL, Login, Password } = cred
|
||||
|
||||
if (!URL || !Login || !Password) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to find existing credential
|
||||
const existingCredential = await prisma.credential.findUnique({
|
||||
where: {
|
||||
hostname_username_browser_url_login: {
|
||||
hostname,
|
||||
username,
|
||||
browser: browserName,
|
||||
url: URL,
|
||||
login: Login
|
||||
}
|
||||
},
|
||||
include: {
|
||||
passwords: {
|
||||
orderBy: { timestamp: 'desc' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (existingCredential) {
|
||||
// Check if password is different from the latest one
|
||||
const latestPassword = existingCredential.passwords[0]
|
||||
|
||||
// Update sync time
|
||||
await prisma.credential.update({
|
||||
where: { id: existingCredential.id },
|
||||
data: { lastSyncTime: currentTime }
|
||||
})
|
||||
|
||||
if (!latestPassword || latestPassword.value !== Password) {
|
||||
// Add new password to history
|
||||
await prisma.password.create({
|
||||
data: {
|
||||
credentialId: existingCredential.id,
|
||||
value: Password,
|
||||
timestamp: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
savedCredentials.push(existingCredential)
|
||||
}
|
||||
} else {
|
||||
// Ensure host exists first
|
||||
await prisma.host.upsert({
|
||||
where: { hostname },
|
||||
update: {},
|
||||
create: { hostname }
|
||||
})
|
||||
|
||||
// Create new credential
|
||||
const newCredential = await prisma.credential.create({
|
||||
data: {
|
||||
hostname,
|
||||
username,
|
||||
browser: browserName,
|
||||
url: URL,
|
||||
login: Login,
|
||||
lastSyncTime: currentTime,
|
||||
passwords: {
|
||||
create: {
|
||||
value: Password,
|
||||
timestamp: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
savedCredentials.push(newCredential)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: '凭据保存成功',
|
||||
count: savedCredentials.length
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存凭据失败:', error)
|
||||
return NextResponse.json({ error: '保存凭据失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCredentialsGet(req: NextRequest) {
|
||||
try {
|
||||
const pathSegments = req.nextUrl.pathname.split('/')
|
||||
const hostnameIndex = pathSegments.indexOf('hosts') + 1
|
||||
const hostname = pathSegments[hostnameIndex]
|
||||
|
||||
if (!hostname) {
|
||||
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentials = await prisma.credential.findMany({
|
||||
where: { hostname },
|
||||
include: {
|
||||
passwords: {
|
||||
orderBy: { timestamp: 'desc' }
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ username: 'asc' },
|
||||
{ browser: 'asc' },
|
||||
{ url: 'asc' }
|
||||
]
|
||||
})
|
||||
|
||||
return NextResponse.json(credentials)
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取凭据失败:', error)
|
||||
return NextResponse.json({ error: '获取凭据失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = withCors(handleCredentialsPost)
|
||||
export const GET = withCors(handleCredentialsGet)
|
||||
201
app/api/hosts/[hostname]/screenshots/route.ts
Normal file
201
app/api/hosts/[hostname]/screenshots/route.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { storeFile } from '@/lib/fileStorage'
|
||||
import { push } from '@/lib/push'
|
||||
import { withCors } from '@/lib/middleware'
|
||||
|
||||
interface WindowInfo {
|
||||
title: string
|
||||
path: string
|
||||
memory: number
|
||||
}
|
||||
|
||||
const timeoutMap = new Map<string, NodeJS.Timeout>()
|
||||
|
||||
async function handleScreenshotUpload(req: NextRequest) {
|
||||
try {
|
||||
const pathSegments = req.nextUrl.pathname.split('/')
|
||||
const hostnameIndex = pathSegments.indexOf('hosts') + 1
|
||||
const hostname = pathSegments[hostnameIndex]
|
||||
|
||||
if (!hostname) {
|
||||
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
|
||||
}
|
||||
|
||||
const formData = await req.formData()
|
||||
const files: File[] = []
|
||||
const windowsInfo: WindowInfo[] = JSON.parse(formData.get('windows_info') as string || '[]')
|
||||
|
||||
// Extract files from formData
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (key.startsWith('screenshot_') && value instanceof File) {
|
||||
files.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
return NextResponse.json({ error: '没有收到文件' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Process screenshots
|
||||
const screenshots = await Promise.all(files.map(async (file, index) => {
|
||||
const ext = file.name ? file.name.split('.').pop() : 'webp'
|
||||
const filename = `${hostname}-${Date.now()}-${Math.round(Math.random() * 1E9)}.${ext}`
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
const storedFile = await storeFile(buffer, filename, file.type, 'screenshot', hostname)
|
||||
|
||||
return {
|
||||
fileId: storedFile.id,
|
||||
objectName: storedFile.objectName,
|
||||
filename: storedFile.filename,
|
||||
contentType: storedFile.contentType,
|
||||
fileSize: storedFile.size,
|
||||
monitorName: formData.get(`monitor_name_${index}`) as string || `Monitor ${index + 1}`
|
||||
}
|
||||
}))
|
||||
|
||||
// Ensure host exists first
|
||||
await prisma.host.upsert({
|
||||
where: { hostname },
|
||||
update: {},
|
||||
create: { hostname }
|
||||
})
|
||||
|
||||
// Create new record
|
||||
const newRecord = await prisma.record.create({
|
||||
data: {
|
||||
hostname,
|
||||
timestamp: new Date(),
|
||||
windows: {
|
||||
create: windowsInfo
|
||||
},
|
||||
screenshots: {
|
||||
create: screenshots
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Handle host status and notifications
|
||||
const host = await prisma.host.findUnique({
|
||||
where: { hostname },
|
||||
select: { lastUpdate: true }
|
||||
})
|
||||
|
||||
const lastUpdate = host?.lastUpdate || new Date(0)
|
||||
|
||||
if (lastUpdate.getTime() === new Date(0).getTime()) {
|
||||
push(`新设备 ${hostname} 上线`)
|
||||
} else if (lastUpdate.getTime() < new Date().getTime() - 1000 * 60) {
|
||||
push(`设备 ${hostname} 上线`)
|
||||
}
|
||||
|
||||
// Clear existing timeout and set new one
|
||||
if (timeoutMap.has(hostname)) {
|
||||
clearTimeout(timeoutMap.get(hostname)!)
|
||||
}
|
||||
timeoutMap.set(hostname, setTimeout(() => {
|
||||
push(`设备 ${hostname} 离线`)
|
||||
timeoutMap.delete(hostname)
|
||||
}, 1000 * 60))
|
||||
|
||||
// Update host last update time
|
||||
await prisma.host.upsert({
|
||||
where: { hostname },
|
||||
update: { lastUpdate: new Date() },
|
||||
create: { hostname, lastUpdate: new Date() }
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: '上传成功',
|
||||
hostname,
|
||||
filesCount: files.length,
|
||||
windowsCount: windowsInfo.length
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
return NextResponse.json({ error: '上传失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetScreenshots(req: NextRequest) {
|
||||
try {
|
||||
const pathSegments = req.nextUrl.pathname.split('/')
|
||||
const hostnameIndex = pathSegments.indexOf('hosts') + 1
|
||||
const hostname = pathSegments[hostnameIndex]
|
||||
|
||||
if (!hostname) {
|
||||
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { searchParams } = req.nextUrl
|
||||
const startTimeParam = searchParams.get('startTime')
|
||||
const endTimeParam = searchParams.get('endTime')
|
||||
|
||||
let startTime: Date | undefined
|
||||
let endTime: Date | undefined
|
||||
|
||||
if (startTimeParam) {
|
||||
const timestamp = isNaN(Number(startTimeParam)) ?
|
||||
new Date(startTimeParam) :
|
||||
new Date(Number(startTimeParam) * 1000)
|
||||
startTime = timestamp
|
||||
}
|
||||
|
||||
if (endTimeParam) {
|
||||
const timestamp = isNaN(Number(endTimeParam)) ?
|
||||
new Date(endTimeParam) :
|
||||
new Date(Number(endTimeParam) * 1000)
|
||||
endTime = timestamp
|
||||
}
|
||||
|
||||
// Build query conditions
|
||||
const whereClause: any = { hostname }
|
||||
|
||||
if (startTime || endTime) {
|
||||
whereClause.timestamp = {}
|
||||
if (startTime) whereClause.timestamp.gte = startTime
|
||||
if (endTime) whereClause.timestamp.lte = endTime
|
||||
}
|
||||
|
||||
// Get records
|
||||
const records = await prisma.record.findMany({
|
||||
where: whereClause,
|
||||
include: {
|
||||
windows: true,
|
||||
screenshots: true
|
||||
},
|
||||
orderBy: {
|
||||
timestamp: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
// Get host info
|
||||
const host = await prisma.host.findUnique({
|
||||
where: { hostname },
|
||||
select: {
|
||||
hostname: true,
|
||||
lastUpdate: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!host) {
|
||||
return NextResponse.json({ error: '未找到主机记录' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
hostname,
|
||||
lastUpdate: host.lastUpdate,
|
||||
records,
|
||||
total: records.length
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取记录失败:', error)
|
||||
return NextResponse.json({ error: '获取记录失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = withCors(handleScreenshotUpload)
|
||||
export const GET = withCors(handleGetScreenshots)
|
||||
70
app/api/hosts/[hostname]/time-distribution/route.ts
Normal file
70
app/api/hosts/[hostname]/time-distribution/route.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { withCors } from '@/lib/middleware'
|
||||
|
||||
async function handleTimeDistribution(req: NextRequest) {
|
||||
try {
|
||||
const pathSegments = req.nextUrl.pathname.split('/')
|
||||
const hostnameIndex = pathSegments.indexOf('hosts') + 1
|
||||
const hostname = pathSegments[hostnameIndex]
|
||||
|
||||
if (!hostname) {
|
||||
return NextResponse.json({ error: '缺少主机名' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Get all records for the hostname and group by hour
|
||||
const records = await prisma.record.findMany({
|
||||
where: { hostname },
|
||||
select: {
|
||||
timestamp: true
|
||||
}
|
||||
})
|
||||
|
||||
// Group by hour
|
||||
interface DistributionEntry {
|
||||
timestamp: number
|
||||
count: number
|
||||
}
|
||||
|
||||
const distribution = records.reduce((acc: DistributionEntry[], record: { timestamp: Date }) => {
|
||||
const timestamp = new Date(record.timestamp)
|
||||
// Create hour-level timestamp (set minutes, seconds, ms to 0)
|
||||
const hourTimestamp = new Date(
|
||||
timestamp.getFullYear(),
|
||||
timestamp.getMonth(),
|
||||
timestamp.getDate(),
|
||||
timestamp.getHours(),
|
||||
0, 0, 0
|
||||
)
|
||||
|
||||
const existingEntry = acc.find(entry =>
|
||||
entry.timestamp === Math.floor(hourTimestamp.getTime() / 1000)
|
||||
)
|
||||
|
||||
if (existingEntry) {
|
||||
existingEntry.count++
|
||||
} else {
|
||||
acc.push({
|
||||
timestamp: Math.floor(hourTimestamp.getTime() / 1000),
|
||||
count: 1
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
// Sort by timestamp
|
||||
distribution.sort((a: DistributionEntry, b: DistributionEntry) => a.timestamp - b.timestamp)
|
||||
|
||||
return NextResponse.json({
|
||||
hostname,
|
||||
distribution
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取时间分布统计失败:', error)
|
||||
return NextResponse.json({ error: '获取时间分布统计失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withCors(handleTimeDistribution)
|
||||
24
app/api/hosts/route.ts
Normal file
24
app/api/hosts/route.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { withCors } from '@/lib/middleware'
|
||||
|
||||
async function handleHostsList(req: NextRequest) {
|
||||
try {
|
||||
const hosts = await prisma.host.findMany({
|
||||
select: {
|
||||
hostname: true,
|
||||
lastUpdate: true
|
||||
},
|
||||
orderBy: {
|
||||
lastUpdate: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(hosts)
|
||||
} catch (error) {
|
||||
console.error('获取主机列表失败:', error)
|
||||
return NextResponse.json({ error: '获取主机列表失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withCors(handleHostsList)
|
||||
44
app/api/screenshots/[fileId]/route.ts
Normal file
44
app/api/screenshots/[fileId]/route.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getFileByObjectName } from '@/lib/fileStorage'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { withCors } from '@/lib/middleware'
|
||||
|
||||
async function handleScreenshotFile(req: NextRequest) {
|
||||
try {
|
||||
const pathSegments = req.nextUrl.pathname.split('/')
|
||||
const fileIdIndex = pathSegments.indexOf('screenshots') + 1
|
||||
const fileId = pathSegments[fileIdIndex]
|
||||
|
||||
if (!fileId) {
|
||||
return NextResponse.json({ error: '缺少文件ID' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 从数据库查找 objectName
|
||||
const screenshot = await prisma.screenshot.findFirst({
|
||||
where: { fileId },
|
||||
select: { objectName: true }
|
||||
})
|
||||
|
||||
if (!screenshot) {
|
||||
return NextResponse.json({ error: '截图不存在' }, { status: 404 })
|
||||
}
|
||||
|
||||
const file = await getFileByObjectName(screenshot.objectName)
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: '文件不存在' }, { status: 404 })
|
||||
}
|
||||
|
||||
return new NextResponse(file.buffer, {
|
||||
headers: {
|
||||
'Content-Type': file.contentType || 'image/webp',
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取截图失败:', error)
|
||||
return NextResponse.json({ error: '获取截图失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withCors(handleScreenshotFile)
|
||||
63
app/api/upload/version/route.ts
Normal file
63
app/api/upload/version/route.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { storeFile } from '@/lib/fileStorage'
|
||||
import { withCors } from '@/lib/middleware'
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
async function handleVersionUpload(req: NextRequest) {
|
||||
try {
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file') as File
|
||||
const version = formData.get('version') as string
|
||||
|
||||
if (!file || !version) {
|
||||
return NextResponse.json({ error: '缺少文件或版本号' }, { status: 400 })
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
|
||||
// Calculate SHA-256
|
||||
const hash = createHash('sha256')
|
||||
hash.update(buffer)
|
||||
const checksum = hash.digest('hex')
|
||||
|
||||
// Store file
|
||||
const filename = `winupdate-${version}-${Date.now()}.exe`
|
||||
const storedFile = await storeFile(buffer, filename, 'application/x-msdownload', 'version')
|
||||
|
||||
// Update all versions to not be latest
|
||||
await prisma.version.updateMany({
|
||||
data: { isLatest: false }
|
||||
})
|
||||
|
||||
// Create new version record
|
||||
const newVersion = await prisma.version.create({
|
||||
data: {
|
||||
version,
|
||||
fileId: storedFile.id,
|
||||
objectName: storedFile.objectName,
|
||||
filename: storedFile.filename,
|
||||
contentType: storedFile.contentType,
|
||||
fileSize: storedFile.size,
|
||||
checksum,
|
||||
isLatest: true
|
||||
}
|
||||
})
|
||||
|
||||
const protocol = req.headers.get('x-forwarded-proto') || 'http'
|
||||
const host = req.headers.get('host')
|
||||
const downloadUrl = `${protocol}://${host}/api/downloads/${storedFile.id}`
|
||||
|
||||
return NextResponse.json({
|
||||
version,
|
||||
download_url: downloadUrl,
|
||||
checksum
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('上传版本失败:', error)
|
||||
return NextResponse.json({ error: '上传失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = withCors(handleVersionUpload)
|
||||
31
app/api/version/route.ts
Normal file
31
app/api/version/route.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { withCors } from '@/lib/middleware'
|
||||
|
||||
async function handleGetVersion(req: NextRequest) {
|
||||
try {
|
||||
const latestVersion = await prisma.version.findFirst({
|
||||
where: { isLatest: true }
|
||||
})
|
||||
|
||||
if (!latestVersion) {
|
||||
return NextResponse.json({ error: 'No version found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const protocol = req.headers.get('x-forwarded-proto') || 'http'
|
||||
const host = req.headers.get('host')
|
||||
const downloadUrl = `${protocol}://${host}/api/downloads/${latestVersion.fileId}`
|
||||
|
||||
return NextResponse.json({
|
||||
version: latestVersion.version,
|
||||
download_url: downloadUrl,
|
||||
checksum: latestVersion.checksum
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取版本信息失败:', error)
|
||||
return NextResponse.json({ error: '获取版本信息失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withCors(handleGetVersion)
|
||||
202
bun.lock
202
bun.lock
@ -4,7 +4,17 @@
|
||||
"": {
|
||||
"name": "winupdate-neo",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.10.1",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/minio": "^7.1.1",
|
||||
"@types/multer": "^1.4.13",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"cors": "^2.8.5",
|
||||
"minio": "^8.0.5",
|
||||
"multer": "^2.0.1",
|
||||
"next": "15.3.4",
|
||||
"prisma": "^6.10.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
},
|
||||
@ -97,6 +107,20 @@
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg=="],
|
||||
|
||||
"@prisma/client": ["@prisma/client@6.10.1", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-Re4pMlcUsQsUTAYMK7EJ4Bw2kg3WfZAAlr8GjORJaK4VOP6LxRQUQ1TuLnxcF42XqGkWQ36q5CQF1yVadANQ6w=="],
|
||||
|
||||
"@prisma/config": ["@prisma/config@6.10.1", "", { "dependencies": { "jiti": "2.4.2" } }, "sha512-kz4/bnqrOrzWo8KzYguN0cden4CzLJJ+2VSpKtF8utHS3l1JS0Lhv6BLwpOX6X9yNreTbZQZwewb+/BMPDCIYQ=="],
|
||||
|
||||
"@prisma/debug": ["@prisma/debug@6.10.1", "", {}, "sha512-k2YT53cWxv9OLjW4zSYTZ6Z7j0gPfCzcr2Mj99qsuvlxr8WAKSZ2NcSR0zLf/mP4oxnYG842IMj3utTgcd7CaA=="],
|
||||
|
||||
"@prisma/engines": ["@prisma/engines@6.10.1", "", { "dependencies": { "@prisma/debug": "6.10.1", "@prisma/engines-version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c", "@prisma/fetch-engine": "6.10.1", "@prisma/get-platform": "6.10.1" } }, "sha512-Q07P5rS2iPwk2IQr/rUQJ42tHjpPyFcbiH7PXZlV81Ryr9NYIgdxcUrwgVOWVm5T7ap02C0dNd1dpnNcSWig8A=="],
|
||||
|
||||
"@prisma/engines-version": ["@prisma/engines-version@6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c", "", {}, "sha512-ZJFTsEqapiTYVzXya6TUKYDFnSWCNegfUiG5ik9fleQva5Sk3DNyyUi7X1+0ZxWFHwHDr6BZV5Vm+iwP+LlciA=="],
|
||||
|
||||
"@prisma/fetch-engine": ["@prisma/fetch-engine@6.10.1", "", { "dependencies": { "@prisma/debug": "6.10.1", "@prisma/engines-version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c", "@prisma/get-platform": "6.10.1" } }, "sha512-clmbG/Jgmrc/n6Y77QcBmAUlq9LrwI9Dbgy4pq5jeEARBpRCWJDJ7PWW1P8p0LfFU0i5fsyO7FqRzRB8mkdS4g=="],
|
||||
|
||||
"@prisma/get-platform": ["@prisma/get-platform@6.10.1", "", { "dependencies": { "@prisma/debug": "6.10.1" } }, "sha512-4CY5ndKylcsce9Mv+VWp5obbR2/86SHOLVV053pwIkhVtT9C9A83yqiqI/5kJM9T1v1u1qco/bYjDKycmei9HA=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
@ -131,14 +155,66 @@
|
||||
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.11", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "postcss": "^8.4.41", "tailwindcss": "4.1.11" } }, "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA=="],
|
||||
|
||||
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
|
||||
|
||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||
|
||||
"@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="],
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/minio": ["@types/minio@7.1.1", "", { "dependencies": { "minio": "*" } }, "sha512-B7OWB7JwIxVBxypiS3gA96gaK4yo2UknGdqmuQsTccZZ/ABiQ2F3fTe9lZIXL6ZuN23l+mWIC3J4CefKNyWjxA=="],
|
||||
|
||||
"@types/multer": ["@types/multer@1.4.13", "", { "dependencies": { "@types/express": "*" } }, "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
|
||||
|
||||
"@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="],
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
|
||||
|
||||
"@zxing/text-encoding": ["@zxing/text-encoding@0.9.0", "", {}, "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA=="],
|
||||
|
||||
"append-field": ["append-field@1.0.0", "", {}, "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="],
|
||||
|
||||
"block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="],
|
||||
|
||||
"browser-or-node": ["browser-or-node@2.1.1", "", {}, "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="],
|
||||
|
||||
"buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001726", "", {}, "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw=="],
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
@ -153,16 +229,70 @@
|
||||
|
||||
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||
|
||||
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
|
||||
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="],
|
||||
|
||||
"filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="],
|
||||
|
||||
"is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
"is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||
@ -187,38 +317,84 @@
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"minio": ["minio@8.0.5", "", { "dependencies": { "async": "^3.2.4", "block-stream2": "^2.1.0", "browser-or-node": "^2.1.1", "buffer-crc32": "^1.0.0", "eventemitter3": "^5.0.1", "fast-xml-parser": "^4.4.1", "ipaddr.js": "^2.0.1", "lodash": "^4.17.21", "mime-types": "^2.1.35", "query-string": "^7.1.3", "stream-json": "^1.8.0", "through2": "^4.0.2", "web-encoding": "^1.1.5", "xml2js": "^0.5.0 || ^0.6.2" } }, "sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
|
||||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||
|
||||
"multer": ["multer@2.0.1", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", "mkdirp": "^0.5.6", "object-assign": "^4.1.1", "type-is": "^1.6.18", "xtend": "^4.0.2" } }, "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"next": ["next@15.3.4", "", { "dependencies": { "@next/env": "15.3.4", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.4", "@next/swc-darwin-x64": "15.3.4", "@next/swc-linux-arm64-gnu": "15.3.4", "@next/swc-linux-arm64-musl": "15.3.4", "@next/swc-linux-x64-gnu": "15.3.4", "@next/swc-linux-x64-musl": "15.3.4", "@next/swc-win32-arm64-msvc": "15.3.4", "@next/swc-win32-x64-msvc": "15.3.4", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"prisma": ["prisma@6.10.1", "", { "dependencies": { "@prisma/config": "6.10.1", "@prisma/engines": "6.10.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-khhlC/G49E4+uyA3T3H5PRBut486HD2bDqE2+rvkU0pwk9IAqGFacLFUyIx9Uw+W2eCtf6XGwsp+/strUwMNPw=="],
|
||||
|
||||
"query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="],
|
||||
|
||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||
|
||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"sharp": ["sharp@0.34.2", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.2", "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.2", "@img/sharp-linux-arm64": "0.34.2", "@img/sharp-linux-s390x": "0.34.2", "@img/sharp-linux-x64": "0.34.2", "@img/sharp-linuxmusl-arm64": "0.34.2", "@img/sharp-linuxmusl-x64": "0.34.2", "@img/sharp-wasm32": "0.34.2", "@img/sharp-win32-arm64": "0.34.2", "@img/sharp-win32-ia32": "0.34.2", "@img/sharp-win32-x64": "0.34.2" } }, "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="],
|
||||
|
||||
"stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="],
|
||||
|
||||
"stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="],
|
||||
|
||||
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
|
||||
|
||||
"strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
|
||||
|
||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||
@ -227,12 +403,34 @@
|
||||
|
||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"web-encoding": ["web-encoding@1.1.5", "", { "dependencies": { "util": "^0.12.3" }, "optionalDependencies": { "@zxing/text-encoding": "0.9.0" } }, "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
|
||||
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
||||
@ -248,5 +446,7 @@
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"tar/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
}
|
||||
}
|
||||
|
||||
12
lib/config.ts
Normal file
12
lib/config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const config = {
|
||||
port: process.env.PORT || 3000,
|
||||
auth: {
|
||||
username: process.env.AUTH_USERNAME || 'admin',
|
||||
password: process.env.AUTH_PASSWORD || 'password'
|
||||
},
|
||||
// For file upload settings
|
||||
upload: {
|
||||
maxFileSize: 10 * 1024 * 1024, // 10MB
|
||||
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'application/x-msdownload']
|
||||
}
|
||||
}
|
||||
216
lib/fileStorage.ts
Normal file
216
lib/fileStorage.ts
Normal file
@ -0,0 +1,216 @@
|
||||
import * as Minio from 'minio'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { minioClient, BUCKET_NAME, initializeMinIO } from './minioClient'
|
||||
|
||||
export interface StoredFile {
|
||||
id: string
|
||||
filename: string
|
||||
contentType: string
|
||||
objectName: string // MinIO 对象名称
|
||||
size: number
|
||||
uploadTime: Date
|
||||
}
|
||||
|
||||
export interface FileMetadata {
|
||||
id: string
|
||||
originalFilename: string
|
||||
contentType: string
|
||||
size: number
|
||||
uploadTime: Date
|
||||
objectName: string
|
||||
}
|
||||
|
||||
// 生成对象名称,使用分层结构优化性能
|
||||
function generateObjectName(type: 'screenshot' | 'version' | 'other', hostname?: string): string {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const uuid = randomUUID()
|
||||
|
||||
if (type === 'screenshot' && hostname) {
|
||||
return `screenshots/${year}/${month}/${day}/${hostname}/${uuid}`
|
||||
} else if (type === 'version') {
|
||||
return `versions/${year}/${month}/${uuid}`
|
||||
} else {
|
||||
return `files/${year}/${month}/${day}/${uuid}`
|
||||
}
|
||||
}
|
||||
|
||||
// 确保 MinIO 已初始化
|
||||
let minioInitialized = false
|
||||
export async function ensureMinIOReady(): Promise<void> {
|
||||
if (!minioInitialized) {
|
||||
const success = await initializeMinIO()
|
||||
if (!success) {
|
||||
throw new Error('MinIO 初始化失败')
|
||||
}
|
||||
minioInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
export async function storeFile(
|
||||
buffer: Buffer,
|
||||
filename: string,
|
||||
contentType: string = 'application/octet-stream',
|
||||
type: 'screenshot' | 'version' | 'other' = 'other',
|
||||
hostname?: string
|
||||
): Promise<StoredFile> {
|
||||
await ensureMinIOReady()
|
||||
|
||||
const id = randomUUID()
|
||||
const ext = filename.split('.').pop() || ''
|
||||
const objectName = `${generateObjectName(type, hostname)}.${ext}`
|
||||
|
||||
// 设置元数据
|
||||
const metadata = {
|
||||
'Content-Type': contentType,
|
||||
'X-Original-Filename': filename,
|
||||
'X-File-ID': id,
|
||||
'X-Upload-Time': new Date().toISOString(),
|
||||
'X-File-Type': type,
|
||||
...(hostname && { 'X-Hostname': hostname })
|
||||
}
|
||||
|
||||
try {
|
||||
// 上传到 MinIO
|
||||
await minioClient.putObject(BUCKET_NAME, objectName, buffer, buffer.length, metadata)
|
||||
|
||||
console.log(`✅ 文件上传成功: ${objectName} (${buffer.length} bytes)`)
|
||||
|
||||
return {
|
||||
id,
|
||||
filename,
|
||||
contentType,
|
||||
objectName,
|
||||
size: buffer.length,
|
||||
uploadTime: new Date()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 文件上传失败:', error)
|
||||
throw new Error(`文件上传失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFile(fileIdOrObjectName: string): Promise<{ buffer: Buffer; contentType: string; filename: string } | null> {
|
||||
try {
|
||||
await ensureMinIOReady()
|
||||
|
||||
let objectName = fileIdOrObjectName
|
||||
|
||||
// 如果看起来是 fileId (UUID格式),我们需要从数据库查找 objectName
|
||||
// 这里简化处理:如果包含 '/' 就认为是 objectName,否则当作 fileId
|
||||
if (!fileIdOrObjectName.includes('/')) {
|
||||
// 这是一个 fileId,需要从数据库查找对应的 objectName
|
||||
// 注意:这里需要在 API 层面处理,因为不同的模型存储在不同表中
|
||||
// 暂时直接使用传入的值,让 API 层传递正确的 objectName
|
||||
console.log(`⚠️ 警告: getFile 收到的可能是 fileId 而不是 objectName: ${fileIdOrObjectName}`)
|
||||
}
|
||||
|
||||
// 获取对象信息
|
||||
const stat = await minioClient.statObject(BUCKET_NAME, objectName)
|
||||
|
||||
// 下载文件
|
||||
const stream = await minioClient.getObject(BUCKET_NAME, objectName)
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', (chunk) => chunks.push(chunk))
|
||||
stream.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks)
|
||||
resolve({
|
||||
buffer,
|
||||
contentType: stat.metaData?.['content-type'] || 'application/octet-stream',
|
||||
filename: stat.metaData?.['x-original-filename'] || objectName.split('/').pop() || 'unknown'
|
||||
})
|
||||
})
|
||||
stream.on('error', (error) => {
|
||||
console.error('❌ 文件下载失败:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 获取文件失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 优化版本:直接通过 objectName 获取文件(用于已知对象名的情况)
|
||||
export async function getFileByObjectName(objectName: string): Promise<{ buffer: Buffer; contentType: string; filename: string } | null> {
|
||||
try {
|
||||
await ensureMinIOReady()
|
||||
|
||||
// 获取对象信息
|
||||
const stat = await minioClient.statObject(BUCKET_NAME, objectName)
|
||||
|
||||
// 下载文件
|
||||
const stream = await minioClient.getObject(BUCKET_NAME, objectName)
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', (chunk) => chunks.push(chunk))
|
||||
stream.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks)
|
||||
resolve({
|
||||
buffer,
|
||||
contentType: stat.metaData?.['content-type'] || 'application/octet-stream',
|
||||
filename: stat.metaData?.['x-original-filename'] || objectName.split('/').pop() || 'unknown'
|
||||
})
|
||||
})
|
||||
stream.on('error', (error) => {
|
||||
console.error('❌ 文件下载失败:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 获取文件失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFile(objectName: string): Promise<boolean> {
|
||||
try {
|
||||
await ensureMinIOReady()
|
||||
|
||||
await minioClient.removeObject(BUCKET_NAME, objectName)
|
||||
console.log(`✅ 文件删除成功: ${objectName}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('❌ 文件删除失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件统计信息
|
||||
export async function getStorageStats(): Promise<{
|
||||
totalObjects: number;
|
||||
totalSize: number;
|
||||
bucketName: string;
|
||||
}> {
|
||||
try {
|
||||
await ensureMinIOReady()
|
||||
|
||||
let totalObjects = 0
|
||||
let totalSize = 0
|
||||
|
||||
const objectsStream = minioClient.listObjects(BUCKET_NAME, '', true)
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
objectsStream.on('data', (obj) => {
|
||||
totalObjects++
|
||||
totalSize += obj.size || 0
|
||||
})
|
||||
objectsStream.on('end', resolve)
|
||||
objectsStream.on('error', reject)
|
||||
})
|
||||
|
||||
return {
|
||||
totalObjects,
|
||||
totalSize,
|
||||
bucketName: BUCKET_NAME
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 获取存储统计失败:', error)
|
||||
return { totalObjects: 0, totalSize: 0, bucketName: BUCKET_NAME }
|
||||
}
|
||||
}
|
||||
67
lib/middleware.ts
Normal file
67
lib/middleware.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { config } from './config'
|
||||
|
||||
export function withAuth(handler: (req: NextRequest) => Promise<NextResponse>) {
|
||||
return async (req: NextRequest) => {
|
||||
const url = new URL(req.url)
|
||||
|
||||
// Skip auth for certain paths
|
||||
if (
|
||||
url.pathname.startsWith('/api/') ||
|
||||
url.pathname.startsWith('/screenshots/') ||
|
||||
url.pathname.startsWith('/downloads/') ||
|
||||
url.pathname.includes('install') ||
|
||||
url.pathname.includes('WinupdateCore') ||
|
||||
req.method === 'POST'
|
||||
) {
|
||||
return handler(req)
|
||||
}
|
||||
|
||||
const authHeader = req.headers.get('authorization')
|
||||
|
||||
if (!authHeader) {
|
||||
return new NextResponse('Authentication required', {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': 'Basic realm="Restricted Access"'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const auth = Buffer.from(authHeader.split(' ')[1], 'base64').toString()
|
||||
const [username, password] = auth.split(':')
|
||||
|
||||
if (username === config.auth.username && password === config.auth.password) {
|
||||
return handler(req)
|
||||
} else {
|
||||
return new NextResponse('Authentication failed', {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': 'Basic realm="Restricted Access"'
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
return new NextResponse('Invalid authentication', {
|
||||
status: 401,
|
||||
headers: {
|
||||
'WWW-Authenticate': 'Basic realm="Restricted Access"'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function withCors(handler: (req: NextRequest) => Promise<NextResponse>) {
|
||||
return async (req: NextRequest) => {
|
||||
const response = await handler(req)
|
||||
|
||||
response.headers.set('Access-Control-Allow-Origin', '*')
|
||||
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
||||
response.headers.set('Access-Control-Expose-Headers', 'Content-Range, X-Content-Range')
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
80
lib/minioClient.ts
Normal file
80
lib/minioClient.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import * as Minio from 'minio'
|
||||
|
||||
// MinIO 配置 - 从环境变量读取
|
||||
export const minioConfig = {
|
||||
endPoint: process.env.MINIO_ENDPOINT || 'localhost',
|
||||
port: parseInt(process.env.MINIO_PORT || '9000'),
|
||||
useSSL: process.env.MINIO_USE_SSL === 'true',
|
||||
accessKey: process.env.MINIO_ACCESS_KEY || '',
|
||||
secretKey: process.env.MINIO_SECRET_KEY || ''
|
||||
}
|
||||
|
||||
export const BUCKET_NAME = process.env.MINIO_BUCKET_NAME || 'winupdate'
|
||||
|
||||
// 验证配置
|
||||
if (!minioConfig.accessKey || !minioConfig.secretKey) {
|
||||
console.error('❌ MinIO 配置不完整,请检查环境变量 MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY')
|
||||
}
|
||||
|
||||
// 创建 MinIO 客户端
|
||||
export const minioClient = new Minio.Client(minioConfig)
|
||||
|
||||
// 测试连接并确保 bucket 存在
|
||||
export async function initializeMinIO(): Promise<boolean> {
|
||||
try {
|
||||
// 测试连接
|
||||
await minioClient.listBuckets()
|
||||
console.log('✅ MinIO 连接成功')
|
||||
|
||||
// 检查 bucket 是否存在
|
||||
const bucketExists = await minioClient.bucketExists(BUCKET_NAME)
|
||||
if (!bucketExists) {
|
||||
console.log(`❌ Bucket "${BUCKET_NAME}" 不存在`)
|
||||
return false
|
||||
}
|
||||
|
||||
console.log(`✅ Bucket "${BUCKET_NAME}" 存在`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('❌ MinIO 连接失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试上传小文件
|
||||
export async function testMinIOUpload(): Promise<boolean> {
|
||||
try {
|
||||
const testData = Buffer.from('Hello MinIO! This is a test file.')
|
||||
const objectName = `test/test-${Date.now()}.txt`
|
||||
|
||||
await minioClient.putObject(BUCKET_NAME, objectName, testData)
|
||||
console.log(`✅ 测试文件上传成功: ${objectName}`)
|
||||
|
||||
// 测试下载
|
||||
const stream = await minioClient.getObject(BUCKET_NAME, objectName)
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', (chunk) => chunks.push(chunk))
|
||||
stream.on('end', () => {
|
||||
const downloadedData = Buffer.concat(chunks)
|
||||
if (downloadedData.toString() === testData.toString()) {
|
||||
console.log('✅ 测试文件下载成功')
|
||||
// 清理测试文件
|
||||
minioClient.removeObject(BUCKET_NAME, objectName).catch(console.error)
|
||||
resolve(true)
|
||||
} else {
|
||||
console.log('❌ 测试文件下载数据不匹配')
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
stream.on('error', (error) => {
|
||||
console.error('❌ 测试文件下载失败:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ MinIO 测试失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
9
lib/prisma.ts
Normal file
9
lib/prisma.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
||||
8
lib/push.ts
Normal file
8
lib/push.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// Simple notification/logging function
|
||||
// In production, you might want to integrate with actual push notification service
|
||||
export function push(message: string) {
|
||||
console.log(`[NOTIFICATION] ${new Date().toISOString()}: ${message}`)
|
||||
|
||||
// Here you could add actual push notification logic
|
||||
// For example, sending to webhook, email, or push notification service
|
||||
}
|
||||
22
package.json
22
package.json
@ -4,14 +4,28 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build": "prisma generate && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"db:generate": "prisma generate",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:reset": "prisma migrate reset",
|
||||
"db:studio": "prisma studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.10.1",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/minio": "^7.1.1",
|
||||
"@types/multer": "^1.4.13",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"cors": "^2.8.5",
|
||||
"minio": "^8.0.5",
|
||||
"multer": "^2.0.1",
|
||||
"next": "15.3.4",
|
||||
"prisma": "^6.10.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"next": "15.3.4"
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
|
||||
122
prisma/migrations/20250628064242_init/migration.sql
Normal file
122
prisma/migrations/20250628064242_init/migration.sql
Normal file
@ -0,0 +1,122 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "hosts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"hostname" TEXT NOT NULL,
|
||||
"lastUpdate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "hosts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "records" (
|
||||
"id" TEXT NOT NULL,
|
||||
"hostname" TEXT NOT NULL,
|
||||
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "records_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "windows" (
|
||||
"id" TEXT NOT NULL,
|
||||
"recordId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"memory" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "windows_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "screenshots" (
|
||||
"id" TEXT NOT NULL,
|
||||
"recordId" TEXT NOT NULL,
|
||||
"fileId" TEXT NOT NULL,
|
||||
"filename" TEXT NOT NULL,
|
||||
"monitorName" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "screenshots_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "credentials" (
|
||||
"id" TEXT NOT NULL,
|
||||
"hostname" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"browser" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"login" TEXT NOT NULL,
|
||||
"lastSyncTime" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "credentials_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "passwords" (
|
||||
"id" TEXT NOT NULL,
|
||||
"credentialId" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "passwords_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "versions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"version" TEXT NOT NULL,
|
||||
"fileId" TEXT NOT NULL,
|
||||
"filename" TEXT NOT NULL,
|
||||
"checksum" TEXT NOT NULL,
|
||||
"uploadTime" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"isLatest" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "versions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "nssm" (
|
||||
"id" TEXT NOT NULL,
|
||||
"fileId" TEXT NOT NULL,
|
||||
"filename" TEXT NOT NULL,
|
||||
"checksum" TEXT NOT NULL,
|
||||
"uploadTime" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "nssm_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "hosts_hostname_key" ON "hosts"("hostname");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "records_hostname_timestamp_idx" ON "records"("hostname", "timestamp");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "records_timestamp_idx" ON "records"("timestamp");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "credentials_hostname_idx" ON "credentials"("hostname");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "credentials_hostname_username_browser_url_login_key" ON "credentials"("hostname", "username", "browser", "url", "login");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "records" ADD CONSTRAINT "records_hostname_fkey" FOREIGN KEY ("hostname") REFERENCES "hosts"("hostname") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "windows" ADD CONSTRAINT "windows_recordId_fkey" FOREIGN KEY ("recordId") REFERENCES "records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "screenshots" ADD CONSTRAINT "screenshots_recordId_fkey" FOREIGN KEY ("recordId") REFERENCES "records"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "credentials" ADD CONSTRAINT "credentials_hostname_fkey" FOREIGN KEY ("hostname") REFERENCES "hosts"("hostname") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "passwords" ADD CONSTRAINT "passwords_credentialId_fkey" FOREIGN KEY ("credentialId") REFERENCES "credentials"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `objectName` to the `nssm` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `objectName` to the `screenshots` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `objectName` to the `versions` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "nssm" ADD COLUMN "contentType" TEXT NOT NULL DEFAULT 'application/x-msdownload',
|
||||
ADD COLUMN "fileSize" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "objectName" TEXT NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "screenshots" ADD COLUMN "contentType" TEXT NOT NULL DEFAULT 'image/webp',
|
||||
ADD COLUMN "fileSize" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "objectName" TEXT NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "versions" ADD COLUMN "contentType" TEXT NOT NULL DEFAULT 'application/x-msdownload',
|
||||
ADD COLUMN "fileSize" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "objectName" TEXT NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "nssm_objectName_idx" ON "nssm"("objectName");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "screenshots_objectName_idx" ON "screenshots"("objectName");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "versions_objectName_idx" ON "versions"("objectName");
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
137
prisma/schema.prisma
Normal file
137
prisma/schema.prisma
Normal file
@ -0,0 +1,137 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Host {
|
||||
id String @id @default(cuid())
|
||||
hostname String @unique
|
||||
lastUpdate DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
records Record[]
|
||||
credentials Credential[]
|
||||
|
||||
@@map("hosts")
|
||||
}
|
||||
|
||||
model Record {
|
||||
id String @id @default(cuid())
|
||||
hostname String
|
||||
timestamp DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
host Host @relation(fields: [hostname], references: [hostname])
|
||||
windows Window[]
|
||||
screenshots Screenshot[]
|
||||
|
||||
@@index([hostname, timestamp])
|
||||
@@index([timestamp])
|
||||
@@map("records")
|
||||
}
|
||||
|
||||
model Window {
|
||||
id String @id @default(cuid())
|
||||
recordId String
|
||||
title String
|
||||
path String
|
||||
memory Int
|
||||
|
||||
// Relations
|
||||
record Record @relation(fields: [recordId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("windows")
|
||||
}
|
||||
|
||||
model Screenshot {
|
||||
id String @id @default(cuid())
|
||||
recordId String
|
||||
fileId String // 保持兼容性
|
||||
objectName String // MinIO 对象名称
|
||||
filename String
|
||||
monitorName String
|
||||
contentType String @default("image/webp")
|
||||
fileSize Int @default(0)
|
||||
|
||||
// Relations
|
||||
record Record @relation(fields: [recordId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([objectName])
|
||||
@@map("screenshots")
|
||||
}
|
||||
|
||||
model Credential {
|
||||
id String @id @default(cuid())
|
||||
hostname String
|
||||
username String
|
||||
browser String
|
||||
url String
|
||||
login String
|
||||
lastSyncTime DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
host Host @relation(fields: [hostname], references: [hostname])
|
||||
passwords Password[]
|
||||
|
||||
@@unique([hostname, username, browser, url, login])
|
||||
@@index([hostname])
|
||||
@@map("credentials")
|
||||
}
|
||||
|
||||
model Password {
|
||||
id String @id @default(cuid())
|
||||
credentialId String
|
||||
value String
|
||||
timestamp DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
credential Credential @relation(fields: [credentialId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("passwords")
|
||||
}
|
||||
|
||||
model Version {
|
||||
id String @id @default(cuid())
|
||||
version String
|
||||
fileId String // 保持兼容性
|
||||
objectName String // MinIO 对象名称
|
||||
filename String
|
||||
checksum String
|
||||
contentType String @default("application/x-msdownload")
|
||||
fileSize Int @default(0)
|
||||
uploadTime DateTime @default(now())
|
||||
isLatest Boolean @default(false)
|
||||
|
||||
@@index([objectName])
|
||||
@@map("versions")
|
||||
}
|
||||
|
||||
model Nssm {
|
||||
id String @id @default(cuid())
|
||||
fileId String // 保持兼容性
|
||||
objectName String // MinIO 对象名称
|
||||
filename String
|
||||
checksum String
|
||||
contentType String @default("application/x-msdownload")
|
||||
fileSize Int @default(0)
|
||||
uploadTime DateTime @default(now())
|
||||
|
||||
@@index([objectName])
|
||||
@@map("nssm")
|
||||
}
|
||||
84
test-file-operations.ts
Normal file
84
test-file-operations.ts
Normal file
@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { storeFile, getFileByObjectName, deleteFile, getStorageStats } from './lib/fileStorage'
|
||||
|
||||
async function testMinIOFileOperations() {
|
||||
console.log('🚀 开始测试 MinIO 文件操作...')
|
||||
|
||||
try {
|
||||
// 测试截图文件上传
|
||||
console.log('\n📤 测试截图文件上传...')
|
||||
const testImageData = Buffer.from('fake image data for testing')
|
||||
const storedScreenshot = await storeFile(
|
||||
testImageData,
|
||||
'test-screenshot.webp',
|
||||
'image/webp',
|
||||
'screenshot',
|
||||
'test-hostname'
|
||||
)
|
||||
console.log('✅ 截图上传成功:', {
|
||||
id: storedScreenshot.id,
|
||||
objectName: storedScreenshot.objectName,
|
||||
size: storedScreenshot.size
|
||||
})
|
||||
|
||||
// 测试版本文件上传
|
||||
console.log('\n📤 测试版本文件上传...')
|
||||
const testExeData = Buffer.from('fake exe data for testing')
|
||||
const storedVersion = await storeFile(
|
||||
testExeData,
|
||||
'winupdate-v1.0.0.exe',
|
||||
'application/x-msdownload',
|
||||
'version'
|
||||
)
|
||||
console.log('✅ 版本文件上传成功:', {
|
||||
id: storedVersion.id,
|
||||
objectName: storedVersion.objectName,
|
||||
size: storedVersion.size
|
||||
})
|
||||
|
||||
// 测试文件下载
|
||||
console.log('\n📥 测试文件下载...')
|
||||
const downloadedScreenshot = await getFileByObjectName(storedScreenshot.objectName)
|
||||
if (downloadedScreenshot && downloadedScreenshot.buffer.equals(testImageData)) {
|
||||
console.log('✅ 截图下载成功,数据一致')
|
||||
} else {
|
||||
console.log('❌ 截图下载失败或数据不一致')
|
||||
}
|
||||
|
||||
const downloadedVersion = await getFileByObjectName(storedVersion.objectName)
|
||||
if (downloadedVersion && downloadedVersion.buffer.equals(testExeData)) {
|
||||
console.log('✅ 版本文件下载成功,数据一致')
|
||||
} else {
|
||||
console.log('❌ 版本文件下载失败或数据不一致')
|
||||
}
|
||||
|
||||
// 测试存储统计
|
||||
console.log('\n📊 测试存储统计...')
|
||||
const stats = await getStorageStats()
|
||||
console.log('✅ 存储统计:', {
|
||||
totalObjects: stats.totalObjects,
|
||||
totalSize: `${(stats.totalSize / 1024).toFixed(2)} KB`,
|
||||
bucketName: stats.bucketName
|
||||
})
|
||||
|
||||
// 清理测试文件
|
||||
console.log('\n🗑️ 清理测试文件...')
|
||||
const deleteScreenshotResult = await deleteFile(storedScreenshot.objectName)
|
||||
const deleteVersionResult = await deleteFile(storedVersion.objectName)
|
||||
|
||||
if (deleteScreenshotResult && deleteVersionResult) {
|
||||
console.log('✅ 测试文件清理成功')
|
||||
} else {
|
||||
console.log('⚠️ 部分测试文件清理失败')
|
||||
}
|
||||
|
||||
console.log('\n🎉 MinIO 文件操作测试完成!')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试过程中出现错误:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
testMinIOFileOperations().catch(console.error)
|
||||
28
test-minio.ts
Normal file
28
test-minio.ts
Normal file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { initializeMinIO, testMinIOUpload } from './lib/minioClient'
|
||||
|
||||
async function testMinIO() {
|
||||
console.log('🚀 开始测试 MinIO 连接...')
|
||||
|
||||
// 测试基础连接
|
||||
const connectionSuccess = await initializeMinIO()
|
||||
if (!connectionSuccess) {
|
||||
console.log('❌ MinIO 连接或配置有问题,请检查:')
|
||||
console.log('1. MinIO 服务是否在 192.168.5.13:9000 运行')
|
||||
console.log('2. 用户名密码是否正确')
|
||||
console.log('3. bucket "winupdate" 是否已创建')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 测试上传下载
|
||||
const uploadSuccess = await testMinIOUpload()
|
||||
if (!uploadSuccess) {
|
||||
console.log('❌ MinIO 上传下载测试失败')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('🎉 MinIO 测试完全成功!可以开始使用了。')
|
||||
}
|
||||
|
||||
testMinIO().catch(console.error)
|
||||
Loading…
x
Reference in New Issue
Block a user