mirror of
https://github.com/ZeroCatDev/ClassworksKV.git
synced 2026-02-04 07:44:40 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88d5062638 | ||
|
|
2f7f78f657 | ||
|
|
80e7214860 | ||
|
|
e7f155b021 | ||
|
|
8c8ebae6b7 | ||
|
|
6e8ba54fd6 | ||
|
|
dc5de6e63a |
4
.gitignore
vendored
4
.gitignore
vendored
@ -145,7 +145,3 @@ dist
|
|||||||
|
|
||||||
prisma/database/data
|
prisma/database/data
|
||||||
data/
|
data/
|
||||||
|
|
||||||
/generated/prisma
|
|
||||||
|
|
||||||
/generated/prisma
|
|
||||||
|
|||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
12
.idea/FixClassworksKV.iml
generated
Normal file
12
.idea/FixClassworksKV.iml
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/FixClassworksKV.iml" filepath="$PROJECT_DIR$/.idea/FixClassworksKV.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
276
API_AUTOAUTH.md
Normal file
276
API_AUTOAUTH.md
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
# AutoAuth 和新增 Apps API 文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档描述了自动授权 (AutoAuth) 相关的 API 接口以及新增的应用管理接口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Apps API 新增接口
|
||||||
|
|
||||||
|
### 1. 通过 namespace 和密码获取 token
|
||||||
|
|
||||||
|
**端点**: `POST /apps/auth/token`
|
||||||
|
|
||||||
|
**描述**: 通过设备的 namespace 和密码进行自动授权,创建新的 AppInstall 并返回 token。
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"namespace": "string (必填)",
|
||||||
|
"password": "string (可选,根据自动授权配置)",
|
||||||
|
"appId": "string (必填)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应** (201 Created):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"token": "string",
|
||||||
|
"deviceType": "string | null",
|
||||||
|
"isReadOnly": boolean,
|
||||||
|
"installedAt": "datetime"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应**:
|
||||||
|
- `400 Bad Request`: 缺少必填字段
|
||||||
|
- `404 Not Found`: 设备不存在或 namespace 不正确
|
||||||
|
- `401 Unauthorized`: 密码不正确或需要提供密码
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 该接口会查找匹配的 AutoAuth 配置
|
||||||
|
- 如果提供了密码,会验证密码是否匹配任何自动授权配置
|
||||||
|
- 如果没有提供密码,会查找无密码的自动授权配置
|
||||||
|
- 根据匹配的 AutoAuth 配置设置 `deviceType` 和 `isReadOnly` 属性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 设置学生名称
|
||||||
|
|
||||||
|
**端点**: `POST /apps/tokens/:token/set-student-name`
|
||||||
|
|
||||||
|
**描述**: 为学生类型的 token 设置名称(更新 note 字段)。
|
||||||
|
|
||||||
|
**URL 参数**:
|
||||||
|
- `token`: AppInstall 的 token
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "string (必填)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应** (200 OK):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"token": "string",
|
||||||
|
"name": "string",
|
||||||
|
"updatedAt": "datetime"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应**:
|
||||||
|
- `400 Bad Request`: 缺少名称或名称不在学生列表中
|
||||||
|
- `403 Forbidden`: token 类型不是 student
|
||||||
|
- `404 Not Found`: token 不存在或设备未设置学生列表
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 只有 `deviceType` 为 `student` 的 token 才能使用此接口
|
||||||
|
- 会验证提供的名称是否存在于设备的 `classworks-list-main` 键值中
|
||||||
|
- 学生列表格式: `[{"id": 1, "name": "学生1"}, {"id": 2, "name": "学生2"}]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AutoAuth 管理 API
|
||||||
|
|
||||||
|
> 🔐 **所有 AutoAuth 管理接口都需要 JWT Account Token 认证**
|
||||||
|
>
|
||||||
|
> **重要**: 只有已绑定账户的设备才能使用这些接口。未绑定账户的设备无法管理 AutoAuth 配置。
|
||||||
|
>
|
||||||
|
> 通过 HTTP Headers 提供:
|
||||||
|
> - `Authorization`: `Bearer {jwt_token}` - 账户的 JWT Token
|
||||||
|
|
||||||
|
### 1. 获取设备的自动授权配置列表
|
||||||
|
|
||||||
|
**端点**: `GET /auto-auth/devices/:uuid/auth-configs`
|
||||||
|
|
||||||
|
**认证**: 需要 JWT Token (账户必须是设备的拥有者)
|
||||||
|
|
||||||
|
**URL 参数**:
|
||||||
|
- `uuid`: 设备的 UUID
|
||||||
|
|
||||||
|
**成功响应** (200 OK):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"configs": [
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"hasPassword": boolean,
|
||||||
|
"deviceType": "string | null",
|
||||||
|
"isReadOnly": boolean,
|
||||||
|
"createdAt": "datetime",
|
||||||
|
"updatedAt": "datetime"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 返回的配置不包含实际的密码哈希值,只显示是否有密码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 创建自动授权配置
|
||||||
|
|
||||||
|
**端点**: `POST /auto-auth/devices/:uuid/auth-configs`
|
||||||
|
|
||||||
|
**认证**: 需要 JWT Token (账户必须是设备的拥有者)
|
||||||
|
|
||||||
|
**URL 参数**:
|
||||||
|
- `uuid`: 设备的 UUID
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"password": "string (可选)",
|
||||||
|
"deviceType": "string (可选: teacher|student|classroom|parent)",
|
||||||
|
"isReadOnly": boolean (可选,默认 false)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应** (201 Created):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"config": {
|
||||||
|
"id": "string",
|
||||||
|
"hasPassword": boolean,
|
||||||
|
"deviceType": "string | null",
|
||||||
|
"isReadOnly": boolean,
|
||||||
|
"createdAt": "datetime"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应**:
|
||||||
|
- `400 Bad Request`: 设备类型无效或密码配置已存在
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 同一设备的密码必须唯一(包括空密码)
|
||||||
|
- `deviceType` 必须是 `teacher`、`student`、`classroom`、`parent` 之一,或为空
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 更新自动授权配置
|
||||||
|
|
||||||
|
**端点**: `PUT /auto-auth/devices/:uuid/auth-configs/:configId`
|
||||||
|
|
||||||
|
**认证**: 需要 JWT Token (账户必须是设备的拥有者)
|
||||||
|
|
||||||
|
**URL 参数**:
|
||||||
|
- `uuid`: 设备的 UUID
|
||||||
|
- `configId`: 自动授权配置的 ID
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"password": "string (可选)",
|
||||||
|
"deviceType": "string (可选: teacher|student|classroom|parent)",
|
||||||
|
"isReadOnly": boolean (可选)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功响应** (200 OK):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"config": {
|
||||||
|
"id": "string",
|
||||||
|
"hasPassword": boolean,
|
||||||
|
"deviceType": "string | null",
|
||||||
|
"isReadOnly": boolean,
|
||||||
|
"updatedAt": "datetime"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应**:
|
||||||
|
- `400 Bad Request`: 设备类型无效或新密码与其他配置冲突
|
||||||
|
- `403 Forbidden`: 无权操作此配置
|
||||||
|
- `404 Not Found`: 配置不存在
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 只能更新属于当前设备的配置
|
||||||
|
- 更新密码时会检查是否与该设备的其他配置冲突
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 删除自动授权配置
|
||||||
|
|
||||||
|
**端点**: `DELETE /auto-auth/devices/:uuid/auth-configs/:configId`
|
||||||
|
|
||||||
|
**认证**: 需要 JWT Token (账户必须是设备的拥有者)
|
||||||
|
|
||||||
|
**URL 参数**:
|
||||||
|
- `uuid`: 设备的 UUID
|
||||||
|
- `configId`: 自动授权配置的 ID
|
||||||
|
|
||||||
|
**成功响应** (204 No Content):
|
||||||
|
- 无响应体
|
||||||
|
|
||||||
|
**错误响应**:
|
||||||
|
- `403 Forbidden`: 无权操作此配置
|
||||||
|
- `404 Not Found`: 配置不存在
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 只能删除属于当前设备的配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 设备类型 (deviceType)
|
||||||
|
|
||||||
|
可选的设备类型值:
|
||||||
|
- `teacher`: 教师
|
||||||
|
- `student`: 学生
|
||||||
|
- `classroom`: 班级一体机
|
||||||
|
- `parent`: 家长
|
||||||
|
- `null`: 未指定类型
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用流程示例
|
||||||
|
|
||||||
|
### 场景 1: 学生使用 namespace 登录
|
||||||
|
|
||||||
|
1. 学生输入班级的 namespace 和密码
|
||||||
|
2. 调用 `POST /apps/auth/token` 获取 token
|
||||||
|
3. 使用返回的 token 访问 KV 存储
|
||||||
|
4. 如果是学生类型,调用 `POST /apps/tokens/:token/set-student-name` 设置自己的名称
|
||||||
|
|
||||||
|
### 场景 2: 管理员配置自动授权
|
||||||
|
|
||||||
|
1. 管理员通过账户登录获取 JWT Token
|
||||||
|
2. 调用 `POST /auto-auth/devices/:uuid/auth-configs` 创建多个授权配置:
|
||||||
|
- 教师密码(deviceType: teacher, isReadOnly: false)
|
||||||
|
- 学生密码(deviceType: student, isReadOnly: false)
|
||||||
|
- 家长密码(deviceType: parent, isReadOnly: true)
|
||||||
|
3. 学生/教师/家长使用对应密码通过 namespace 登录
|
||||||
|
|
||||||
|
**注意**: 设备必须已绑定到管理员的账户才能配置 AutoAuth
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **密码安全**: 所有密码都使用 bcrypt 进行哈希存储
|
||||||
|
2. **唯一性约束**:
|
||||||
|
- 同一设备的 namespace 必须唯一
|
||||||
|
- 同一设备的 AutoAuth 密码必须唯一(包括 null)
|
||||||
|
3. **级联删除**: 删除设备会级联删除所有相关的 AutoAuth 配置和 AppInstall 记录
|
||||||
|
4. **只读限制**: isReadOnly 为 true 的 token 在 KV 操作中会受到写入限制
|
||||||
|
5. **账户绑定要求**: 只有已绑定账户的设备才能管理 AutoAuth 配置,未绑定账户的设备无法使用 AutoAuth 管理接口
|
||||||
0
API_QUICK_REFERENCE.md
Normal file
0
API_QUICK_REFERENCE.md
Normal file
@ -1,4 +1,4 @@
|
|||||||
FROM node:24-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
129
MIGRATION_CHECKLIST.md
Normal file
129
MIGRATION_CHECKLIST.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# Refresh Token系统迁移检查清单
|
||||||
|
|
||||||
|
## 🔧 服务端迁移
|
||||||
|
|
||||||
|
### 数据库
|
||||||
|
- [ ] 运行Prisma迁移: `npx prisma migrate dev --name add_refresh_token_system`
|
||||||
|
- [ ] 验证Account表新增字段: refreshToken, refreshTokenExpiry, tokenVersion
|
||||||
|
|
||||||
|
### 环境配置
|
||||||
|
- [ ] 添加 `ACCESS_TOKEN_EXPIRES_IN=15m`
|
||||||
|
- [ ] 添加 `REFRESH_TOKEN_EXPIRES_IN=7d`
|
||||||
|
- [ ] 添加 `REFRESH_TOKEN_SECRET=your-refresh-token-secret`
|
||||||
|
- [ ] (可选)配置RSA密钥对
|
||||||
|
|
||||||
|
### 代码验证
|
||||||
|
- [ ] `utils/tokenManager.js` 文件已创建
|
||||||
|
- [ ] `utils/jwt.js` 已更新(保持向后兼容)
|
||||||
|
- [ ] `middleware/jwt-auth.js` 已升级
|
||||||
|
- [ ] `routes/accounts.js` 新增refresh相关端点
|
||||||
|
|
||||||
|
## 🖥️ 前端迁移
|
||||||
|
|
||||||
|
### OAuth回调处理
|
||||||
|
- [ ] 更新回调URL参数解析(支持access_token和refresh_token)
|
||||||
|
- [ ] 保持对旧版token参数的兼容性
|
||||||
|
- [ ] 实现TokenManager类
|
||||||
|
|
||||||
|
### Token管理
|
||||||
|
- [ ] 实现Token刷新逻辑
|
||||||
|
- [ ] 添加请求拦截器检查X-New-Access-Token响应头
|
||||||
|
- [ ] 实现401错误自动重试机制
|
||||||
|
- [ ] 添加登出功能(单设备/全设备)
|
||||||
|
|
||||||
|
### 存储策略
|
||||||
|
- [ ] Access Token存储(localStorage/sessionStorage)
|
||||||
|
- [ ] Refresh Token安全存储
|
||||||
|
- [ ] 实现Token清理逻辑
|
||||||
|
|
||||||
|
## 🧪 测试验证
|
||||||
|
|
||||||
|
### 功能测试
|
||||||
|
- [ ] OAuth登录流程测试
|
||||||
|
- [ ] Token自动刷新测试
|
||||||
|
- [ ] 手动refresh接口测试
|
||||||
|
- [ ] 登出功能测试(单设备)
|
||||||
|
- [ ] 登出功能测试(全设备)
|
||||||
|
- [ ] Token信息查看测试
|
||||||
|
|
||||||
|
### 兼容性测试
|
||||||
|
- [ ] 旧版JWT token仍然有效
|
||||||
|
- [ ] 新旧token混合使用场景
|
||||||
|
- [ ] API向后兼容性验证
|
||||||
|
|
||||||
|
### 错误处理测试
|
||||||
|
- [ ] 过期token处理
|
||||||
|
- [ ] 无效refresh token处理
|
||||||
|
- [ ] 网络错误重试
|
||||||
|
- [ ] 并发刷新场景
|
||||||
|
|
||||||
|
## 📊 监控配置
|
||||||
|
|
||||||
|
### 日志记录
|
||||||
|
- [ ] Token生成日志
|
||||||
|
- [ ] Token刷新日志
|
||||||
|
- [ ] 认证失败日志
|
||||||
|
- [ ] 登出操作日志
|
||||||
|
|
||||||
|
### 性能监控
|
||||||
|
- [ ] Token刷新频率统计
|
||||||
|
- [ ] API响应时间监控
|
||||||
|
- [ ] 数据库查询性能
|
||||||
|
|
||||||
|
## 🔒 安全检查
|
||||||
|
|
||||||
|
### Token安全
|
||||||
|
- [ ] 密钥强度验证
|
||||||
|
- [ ] Token过期时间配置合理
|
||||||
|
- [ ] HTTPS传输确认
|
||||||
|
- [ ] 敏感信息不在日志中暴露
|
||||||
|
|
||||||
|
### 访问控制
|
||||||
|
- [ ] Token撤销功能正常
|
||||||
|
- [ ] 版本控制机制有效
|
||||||
|
- [ ] 设备隔离正确
|
||||||
|
|
||||||
|
## 📚 文档检查
|
||||||
|
|
||||||
|
- [ ] API文档已更新
|
||||||
|
- [ ] 前端集成指南已提供
|
||||||
|
- [ ] 迁移步骤文档完整
|
||||||
|
- [ ] 错误处理指南清晰
|
||||||
|
|
||||||
|
## 🚀 上线准备
|
||||||
|
|
||||||
|
### 部署前
|
||||||
|
- [ ] 代码review完成
|
||||||
|
- [ ] 单元测试通过
|
||||||
|
- [ ] 集成测试通过
|
||||||
|
- [ ] 性能测试通过
|
||||||
|
|
||||||
|
### 部署时
|
||||||
|
- [ ] 数据库迁移执行
|
||||||
|
- [ ] 环境变量配置
|
||||||
|
- [ ] 服务重启验证
|
||||||
|
- [ ] 健康检查通过
|
||||||
|
|
||||||
|
### 部署后
|
||||||
|
- [ ] 新用户登录测试
|
||||||
|
- [ ] 现有用户功能正常
|
||||||
|
- [ ] 监控指标正常
|
||||||
|
- [ ] 错误日志检查
|
||||||
|
|
||||||
|
## 🔄 回滚计划
|
||||||
|
|
||||||
|
### 紧急回滚
|
||||||
|
- [ ] 回滚代码到上一版本
|
||||||
|
- [ ] 恢复原环境变量
|
||||||
|
- [ ] 数据库回滚方案(如需要)
|
||||||
|
|
||||||
|
### 数据迁移回滚
|
||||||
|
- [ ] 备份新增字段数据
|
||||||
|
- [ ] 移除新增字段的迁移脚本
|
||||||
|
- [ ] 验证旧版功能正常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**检查完成人员**: ___________
|
||||||
|
**检查完成时间**: ___________
|
||||||
|
**环境**: [ ] 开发 [ ] 测试 [ ] 生产
|
||||||
0
NEW_APIS_SUMMARY.md
Normal file
0
NEW_APIS_SUMMARY.md
Normal file
489
REFRESH_TOKEN_API.md
Normal file
489
REFRESH_TOKEN_API.md
Normal file
@ -0,0 +1,489 @@
|
|||||||
|
# Refresh Token系统API文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
ClassworksKV现在支持标准的Refresh Token认证系统,提供更安全的用户认证机制。新系统包含:
|
||||||
|
|
||||||
|
- **Access Token**: 短期令牌(默认15分钟),用于API访问
|
||||||
|
- **Refresh Token**: 长期令牌(默认7天),用于刷新Access Token
|
||||||
|
- **Token版本控制**: 支持令牌失效和安全登出
|
||||||
|
- **向后兼容**: 支持旧版JWT令牌
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
可以通过环境变量配置token系统:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Access Token配置
|
||||||
|
ACCESS_TOKEN_EXPIRES_IN=15m # Access Token过期时间
|
||||||
|
REFRESH_TOKEN_EXPIRES_IN=7d # Refresh Token过期时间
|
||||||
|
|
||||||
|
# 密钥配置(HS256算法)
|
||||||
|
JWT_SECRET=your-access-token-secret
|
||||||
|
REFRESH_TOKEN_SECRET=your-refresh-token-secret
|
||||||
|
|
||||||
|
# RSA密钥配置(RS256算法,可选)
|
||||||
|
JWT_ALG=RS256
|
||||||
|
ACCESS_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..."
|
||||||
|
ACCESS_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n..."
|
||||||
|
REFRESH_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..."
|
||||||
|
REFRESH_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## API端点
|
||||||
|
|
||||||
|
### 1. OAuth登录回调
|
||||||
|
|
||||||
|
OAuth登录成功后,系统会返回令牌对。
|
||||||
|
|
||||||
|
**回调URL参数(新版):**
|
||||||
|
```
|
||||||
|
https://your-frontend.com/?access_token=eyJ...&refresh_token=eyJ...&expires_in=15m&success=true&provider=github
|
||||||
|
```
|
||||||
|
|
||||||
|
**旧版兼容参数:**
|
||||||
|
```
|
||||||
|
https://your-frontend.com/?token=eyJ...&success=true&provider=github
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 刷新访问令牌
|
||||||
|
|
||||||
|
当Access Token即将过期或已过期时,使用Refresh Token获取新的Access Token。
|
||||||
|
|
||||||
|
**端点:** `POST /api/accounts/refresh`
|
||||||
|
|
||||||
|
**请求体:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应(成功):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "令牌刷新成功",
|
||||||
|
"data": {
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"expires_in": "15m",
|
||||||
|
"account": {
|
||||||
|
"id": "clxxxx",
|
||||||
|
"provider": "github",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "User Name",
|
||||||
|
"avatarUrl": "https://..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "刷新令牌已过期"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误状态码:**
|
||||||
|
- `400`: 缺少刷新令牌
|
||||||
|
- `401`: 无效的刷新令牌、令牌已过期、账户不存在、令牌版本不匹配
|
||||||
|
|
||||||
|
### 3. 登出(当前设备)
|
||||||
|
|
||||||
|
撤销当前设备的Refresh Token,但不影响其他设备。
|
||||||
|
|
||||||
|
**端点:** `POST /api/accounts/logout`
|
||||||
|
|
||||||
|
**请求头:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "登出成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 登出所有设备
|
||||||
|
|
||||||
|
撤销账户的所有令牌,强制所有设备重新登录。
|
||||||
|
|
||||||
|
**端点:** `POST /api/accounts/logout-all`
|
||||||
|
|
||||||
|
**请求头:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "已从所有设备登出"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 获取令牌信息
|
||||||
|
|
||||||
|
查看当前令牌的详细信息和状态。
|
||||||
|
|
||||||
|
**端点:** `GET /api/accounts/token-info`
|
||||||
|
|
||||||
|
**请求头:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"accountId": "clxxxx",
|
||||||
|
"tokenType": "access",
|
||||||
|
"tokenVersion": 1,
|
||||||
|
"issuedAt": "2024-11-01T08:00:00.000Z",
|
||||||
|
"expiresAt": "2024-11-01T08:15:00.000Z",
|
||||||
|
"expiresIn": 900,
|
||||||
|
"isExpired": false,
|
||||||
|
"isLegacyToken": false,
|
||||||
|
"hasRefreshToken": true,
|
||||||
|
"refreshTokenExpiry": "2024-11-08T08:00:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 自动刷新机制
|
||||||
|
|
||||||
|
### 响应头刷新
|
||||||
|
|
||||||
|
当Access Token剩余有效期少于5分钟时,系统会在响应头中提供新的Access Token:
|
||||||
|
|
||||||
|
**响应头:**
|
||||||
|
```
|
||||||
|
X-New-Access-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
X-Token-Refreshed: true
|
||||||
|
```
|
||||||
|
|
||||||
|
前端应检查这些响应头并更新本地存储的token。
|
||||||
|
|
||||||
|
## 前端集成示例
|
||||||
|
|
||||||
|
### JavaScript/TypeScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class TokenManager {
|
||||||
|
constructor() {
|
||||||
|
this.accessToken = localStorage.getItem('access_token');
|
||||||
|
this.refreshToken = localStorage.getItem('refresh_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置令牌对
|
||||||
|
setTokens(accessToken, refreshToken) {
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
localStorage.setItem('access_token', accessToken);
|
||||||
|
localStorage.setItem('refresh_token', refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除令牌
|
||||||
|
clearTokens() {
|
||||||
|
this.accessToken = null;
|
||||||
|
this.refreshToken = null;
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新访问令牌
|
||||||
|
async refreshAccessToken() {
|
||||||
|
if (!this.refreshToken) {
|
||||||
|
throw new Error('No refresh token available');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/accounts/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
refresh_token: this.refreshToken
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
this.accessToken = data.data.access_token;
|
||||||
|
localStorage.setItem('access_token', this.accessToken);
|
||||||
|
return this.accessToken;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.clearTokens();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API请求拦截器
|
||||||
|
async request(url, options = {}) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${this.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否有新的访问令牌
|
||||||
|
const newAccessToken = response.headers.get('X-New-Access-Token');
|
||||||
|
if (newAccessToken) {
|
||||||
|
this.accessToken = newAccessToken;
|
||||||
|
localStorage.setItem('access_token', newAccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果token过期,尝试刷新
|
||||||
|
if (response.status === 401) {
|
||||||
|
try {
|
||||||
|
await this.refreshAccessToken();
|
||||||
|
// 重试请求
|
||||||
|
return this.request(url, options);
|
||||||
|
} catch (refreshError) {
|
||||||
|
// 刷新失败,重定向到登录页
|
||||||
|
window.location.href = '/login';
|
||||||
|
throw refreshError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
await this.request('/api/accounts/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
} finally {
|
||||||
|
this.clearTokens();
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出所有设备
|
||||||
|
async logoutAll() {
|
||||||
|
try {
|
||||||
|
await this.request('/api/accounts/logout-all', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout all error:', error);
|
||||||
|
} finally {
|
||||||
|
this.clearTokens();
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
const tokenManager = new TokenManager();
|
||||||
|
|
||||||
|
// OAuth回调处理
|
||||||
|
function handleOAuthCallback() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const accessToken = params.get('access_token');
|
||||||
|
const refreshToken = params.get('refresh_token');
|
||||||
|
|
||||||
|
if (accessToken && refreshToken) {
|
||||||
|
tokenManager.setTokens(accessToken, refreshToken);
|
||||||
|
// 重定向到应用主页
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
} else {
|
||||||
|
// 处理旧版回调
|
||||||
|
const legacyToken = params.get('token');
|
||||||
|
if (legacyToken) {
|
||||||
|
tokenManager.setTokens(legacyToken, null);
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API调用示例
|
||||||
|
async function getUserProfile() {
|
||||||
|
try {
|
||||||
|
const response = await tokenManager.request('/api/accounts/profile');
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get user profile:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### React Hook
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const tokenManager = new TokenManager();
|
||||||
|
|
||||||
|
const checkAuth = useCallback(async () => {
|
||||||
|
if (!tokenManager.accessToken) {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await tokenManager.request('/api/accounts/profile');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setUser(data.data);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
} else {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuth();
|
||||||
|
}, [checkAuth]);
|
||||||
|
|
||||||
|
const login = useCallback((accessToken, refreshToken) => {
|
||||||
|
tokenManager.setTokens(accessToken, refreshToken);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
checkAuth();
|
||||||
|
}, [checkAuth]);
|
||||||
|
|
||||||
|
const logout = useCallback(async () => {
|
||||||
|
await tokenManager.logout();
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setUser(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAuthenticated,
|
||||||
|
user,
|
||||||
|
loading,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
tokenManager,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全考虑
|
||||||
|
|
||||||
|
### 1. Token存储
|
||||||
|
- **Access Token**: 可以存储在内存或localStorage中
|
||||||
|
- **Refresh Token**: 建议存储在httpOnly cookie中(需要后端支持),或者安全的本地存储
|
||||||
|
|
||||||
|
### 2. HTTPS
|
||||||
|
- 生产环境必须使用HTTPS传输令牌
|
||||||
|
|
||||||
|
### 3. Token轮换
|
||||||
|
- 系统支持令牌版本控制,可以快速失效所有令牌
|
||||||
|
|
||||||
|
### 4. 过期时间
|
||||||
|
- Access Token短期有效(15分钟)
|
||||||
|
- Refresh Token长期有效(7天)
|
||||||
|
- 可根据安全需求调整
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
### 从旧版JWT系统迁移
|
||||||
|
|
||||||
|
1. **前端更新**:
|
||||||
|
- 更新OAuth回调处理逻辑
|
||||||
|
- 实现Token刷新机制
|
||||||
|
- 处理新的响应头
|
||||||
|
|
||||||
|
2. **向后兼容**:
|
||||||
|
- 旧版JWT token仍然有效
|
||||||
|
- 系统会在响应中标记`isLegacyToken: true`
|
||||||
|
- 建议用户重新登录获取新令牌
|
||||||
|
|
||||||
|
3. **数据库迁移**:
|
||||||
|
```bash
|
||||||
|
# 运行Prisma迁移
|
||||||
|
npm run prisma migrate dev --name add_refresh_token_support
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### 常见错误
|
||||||
|
|
||||||
|
| 错误代码 | 错误信息 | 处理方式 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| 401 | JWT token已过期 | 使用refresh token刷新 |
|
||||||
|
| 401 | 无效的刷新令牌 | 重新登录 |
|
||||||
|
| 401 | 令牌版本不匹配 | 重新登录 |
|
||||||
|
| 401 | 账户不存在 | 重新登录 |
|
||||||
|
|
||||||
|
### 错误处理流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[API请求] --> B{Token有效?}
|
||||||
|
B -->|是| C[返回数据]
|
||||||
|
B -->|否,401错误| D{有Refresh Token?}
|
||||||
|
D -->|否| E[重定向登录]
|
||||||
|
D -->|是| F[尝试刷新Token]
|
||||||
|
F --> G{刷新成功?}
|
||||||
|
G -->|是| H[重试原请求]
|
||||||
|
G -->|否| E
|
||||||
|
H --> C
|
||||||
|
```
|
||||||
|
|
||||||
|
## 监控和日志
|
||||||
|
|
||||||
|
### 建议监控指标
|
||||||
|
|
||||||
|
- Token刷新频率
|
||||||
|
- Token刷新失败率
|
||||||
|
- 用户登出频率
|
||||||
|
- 异常登录尝试
|
||||||
|
|
||||||
|
### 日志记录
|
||||||
|
|
||||||
|
系统会记录以下事件:
|
||||||
|
- Token生成
|
||||||
|
- Token刷新
|
||||||
|
- Token撤销
|
||||||
|
- 认证失败
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 1. Token缓存
|
||||||
|
- 在内存中缓存已验证的token(适用于高并发场景)
|
||||||
|
|
||||||
|
### 2. 数据库优化
|
||||||
|
- 为`refreshToken`字段添加索引
|
||||||
|
- 定期清理过期的refresh token
|
||||||
|
|
||||||
|
### 3. 前端优化
|
||||||
|
- 实现Token预刷新机制
|
||||||
|
- 使用Web Workers处理Token逻辑
|
||||||
112
REFRESH_TOKEN_QUICKSTART.md
Normal file
112
REFRESH_TOKEN_QUICKSTART.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# Refresh Token系统 - 快速使用指南
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 环境变量配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 添加到 .env 文件
|
||||||
|
ACCESS_TOKEN_EXPIRES_IN=15m
|
||||||
|
REFRESH_TOKEN_EXPIRES_IN=7d
|
||||||
|
REFRESH_TOKEN_SECRET=your-refresh-token-secret-change-this
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据库迁移
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev --name add_refresh_token_system
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 新的OAuth回调参数
|
||||||
|
|
||||||
|
登录成功后,回调URL现在包含:
|
||||||
|
```
|
||||||
|
?access_token=eyJ...&refresh_token=eyJ...&expires_in=15m&success=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 核心API
|
||||||
|
|
||||||
|
### 刷新Token
|
||||||
|
```http
|
||||||
|
POST /api/accounts/refresh
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"refresh_token": "eyJ..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 登出当前设备
|
||||||
|
```http
|
||||||
|
POST /api/accounts/logout
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 登出所有设备
|
||||||
|
```http
|
||||||
|
POST /api/accounts/logout-all
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💻 前端集成
|
||||||
|
|
||||||
|
### 基础Token管理
|
||||||
|
```javascript
|
||||||
|
class TokenManager {
|
||||||
|
setTokens(accessToken, refreshToken) {
|
||||||
|
localStorage.setItem('access_token', accessToken);
|
||||||
|
localStorage.setItem('refresh_token', refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshToken() {
|
||||||
|
const refreshToken = localStorage.getItem('refresh_token');
|
||||||
|
const response = await fetch('/api/accounts/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ refresh_token: refreshToken })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
localStorage.setItem('access_token', data.data.access_token);
|
||||||
|
return data.data.access_token;
|
||||||
|
}
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自动刷新拦截器
|
||||||
|
```javascript
|
||||||
|
// 检查响应头中的新token
|
||||||
|
const newToken = response.headers.get('X-New-Access-Token');
|
||||||
|
if (newToken) {
|
||||||
|
localStorage.setItem('access_token', newToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 401错误时自动刷新
|
||||||
|
if (response.status === 401) {
|
||||||
|
await tokenManager.refreshToken();
|
||||||
|
// 重试请求
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 安全特性
|
||||||
|
|
||||||
|
- ✅ 短期Access Token(15分钟)
|
||||||
|
- ✅ 长期Refresh Token(7天)
|
||||||
|
- ✅ Token版本控制
|
||||||
|
- ✅ 设备级登出
|
||||||
|
- ✅ 全局登出
|
||||||
|
- ✅ 自动刷新机制
|
||||||
|
- ✅ 向后兼容
|
||||||
|
|
||||||
|
## 🔄 迁移步骤
|
||||||
|
|
||||||
|
1. **更新环境变量**
|
||||||
|
2. **运行数据库迁移**
|
||||||
|
3. **更新前端OAuth回调处理**
|
||||||
|
4. **实现Token刷新逻辑**
|
||||||
|
5. **测试登出功能**
|
||||||
|
|
||||||
|
详细文档请参考:`REFRESH_TOKEN_API.md`
|
||||||
174
REFRESH_TOKEN_SUMMARY.md
Normal file
174
REFRESH_TOKEN_SUMMARY.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# 账户登录密钥系统重构完成报告
|
||||||
|
|
||||||
|
## 📋 项目概述
|
||||||
|
|
||||||
|
已成功重构ClassworksKV的账户登录密钥系统,从单一JWT令牌升级为标准的Refresh Token系统,大幅提升了安全性和用户体验。
|
||||||
|
|
||||||
|
## ✅ 完成的工作
|
||||||
|
|
||||||
|
### 1. 数据库架构更新
|
||||||
|
- 在`Account`模型中添加了`refreshToken`、`refreshTokenExpiry`和`tokenVersion`字段
|
||||||
|
- 支持令牌版本控制,可快速失效所有设备的令牌
|
||||||
|
- 向后兼容现有数据
|
||||||
|
|
||||||
|
### 2. 核心Token管理系统
|
||||||
|
- **创建 `utils/tokenManager.js`**: 全新的令牌管理核心
|
||||||
|
- 生成Access Token(15分钟有效期)
|
||||||
|
- 生成Refresh Token(7天有效期)
|
||||||
|
- 支持HS256和RS256算法
|
||||||
|
- 令牌刷新和撤销功能
|
||||||
|
- 安全验证机制
|
||||||
|
|
||||||
|
- **重构 `utils/jwt.js`**: 保持向后兼容性
|
||||||
|
- 重新导出新的令牌管理功能
|
||||||
|
- 保留旧版API供现有代码使用
|
||||||
|
|
||||||
|
### 3. 认证中间件升级
|
||||||
|
- **更新 `middleware/jwt-auth.js`**:
|
||||||
|
- 支持新的Access Token验证
|
||||||
|
- 自动检测即将过期的令牌并在响应头提供新令牌
|
||||||
|
- 向后兼容旧版JWT令牌
|
||||||
|
- 新增可选认证中间件
|
||||||
|
|
||||||
|
### 4. API端点扩展
|
||||||
|
- **更新 `routes/accounts.js`**:
|
||||||
|
- OAuth回调现在返回令牌对(access_token + refresh_token)
|
||||||
|
- 新增 `/api/accounts/refresh` - 刷新访问令牌
|
||||||
|
- 新增 `/api/accounts/logout` - 单设备登出
|
||||||
|
- 新增 `/api/accounts/logout-all` - 全设备登出
|
||||||
|
- 新增 `/api/accounts/token-info` - 查看令牌状态
|
||||||
|
|
||||||
|
### 5. 安全特性
|
||||||
|
- **短期Access Token**: 默认15分钟,降低泄露风险
|
||||||
|
- **长期Refresh Token**: 默认7天,用户体验友好
|
||||||
|
- **令牌版本控制**: 支持立即失效所有设备的令牌
|
||||||
|
- **自动刷新机制**: 在令牌即将过期时自动提供新令牌
|
||||||
|
- **设备级管理**: 支持单设备或全设备登出
|
||||||
|
|
||||||
|
## 📚 文档输出
|
||||||
|
|
||||||
|
### 1. 详细API文档
|
||||||
|
**文件**: `REFRESH_TOKEN_API.md`
|
||||||
|
- 完整的API接口说明
|
||||||
|
- 前端集成示例(JavaScript/React)
|
||||||
|
- 安全考虑和最佳实践
|
||||||
|
- 错误处理指南
|
||||||
|
- 性能优化建议
|
||||||
|
|
||||||
|
### 2. 快速使用指南
|
||||||
|
**文件**: `REFRESH_TOKEN_QUICKSTART.md`
|
||||||
|
- 环境配置说明
|
||||||
|
- 核心API使用方法
|
||||||
|
- 前端集成代码示例
|
||||||
|
- 迁移步骤指导
|
||||||
|
|
||||||
|
## 🔧 配置说明
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
```bash
|
||||||
|
# Access Token配置
|
||||||
|
ACCESS_TOKEN_EXPIRES_IN=15m # 访问令牌过期时间
|
||||||
|
REFRESH_TOKEN_EXPIRES_IN=7d # 刷新令牌过期时间
|
||||||
|
|
||||||
|
# 密钥配置
|
||||||
|
JWT_SECRET=your-access-token-secret # Access Token密钥
|
||||||
|
REFRESH_TOKEN_SECRET=your-refresh-token-secret # Refresh Token密钥
|
||||||
|
|
||||||
|
# 可选:RSA算法配置
|
||||||
|
JWT_ALG=RS256
|
||||||
|
ACCESS_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----..."
|
||||||
|
ACCESS_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----..."
|
||||||
|
REFRESH_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----..."
|
||||||
|
REFRESH_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 部署步骤
|
||||||
|
|
||||||
|
### 1. 数据库迁移
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev --name add_refresh_token_system
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 环境变量更新
|
||||||
|
```bash
|
||||||
|
# 添加新的环境变量到 .env 文件
|
||||||
|
echo "ACCESS_TOKEN_EXPIRES_IN=15m" >> .env
|
||||||
|
echo "REFRESH_TOKEN_EXPIRES_IN=7d" >> .env
|
||||||
|
echo "REFRESH_TOKEN_SECRET=your-refresh-token-secret-change-this" >> .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 前端更新
|
||||||
|
- 更新OAuth回调处理逻辑
|
||||||
|
- 实现Token刷新机制
|
||||||
|
- 添加自动重试逻辑
|
||||||
|
|
||||||
|
## 🔄 向后兼容性
|
||||||
|
|
||||||
|
- ✅ 现有JWT令牌继续有效
|
||||||
|
- ✅ 旧版API端点保持不变
|
||||||
|
- ✅ 渐进式迁移支持
|
||||||
|
- ✅ 中间件自动检测令牌类型
|
||||||
|
|
||||||
|
## 📊 系统架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||||
|
│ 前端应用 │ │ ClassworksKV │ │ 数据库 │
|
||||||
|
│ │ │ 服务端 │ │ │
|
||||||
|
├─────────────────┤ ├──────────────────┤ ├─────────────────┤
|
||||||
|
│ • Token存储 │◄──►│ • OAuth认证 │◄──►│ • Account表 │
|
||||||
|
│ • 自动刷新 │ │ • Token生成 │ │ • refreshToken │
|
||||||
|
│ • 请求拦截 │ │ • Token验证 │ │ • tokenVersion │
|
||||||
|
│ • 错误处理 │ │ • Token刷新 │ │ • 过期时间 │
|
||||||
|
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛡️ 安全增强
|
||||||
|
|
||||||
|
### 改进前(旧系统)
|
||||||
|
- 单一JWT令牌
|
||||||
|
- 长期有效(7天)
|
||||||
|
- 泄露风险高
|
||||||
|
- 无法远程登出
|
||||||
|
|
||||||
|
### 改进后(新系统)
|
||||||
|
- 双令牌系统
|
||||||
|
- Access Token短期(15分钟)
|
||||||
|
- Refresh Token长期(7天)
|
||||||
|
- 令牌版本控制
|
||||||
|
- 设备级管理
|
||||||
|
- 自动刷新机制
|
||||||
|
|
||||||
|
## 📈 性能考虑
|
||||||
|
|
||||||
|
- **数据库**: 为refreshToken字段添加索引
|
||||||
|
- **内存**: Token缓存机制(可选)
|
||||||
|
- **网络**: 预刷新机制减少延迟
|
||||||
|
- **存储**: 定期清理过期令牌
|
||||||
|
|
||||||
|
## 🧪 测试建议
|
||||||
|
|
||||||
|
### 功能测试
|
||||||
|
1. OAuth登录流程测试
|
||||||
|
2. Token刷新功能测试
|
||||||
|
3. 登出功能测试
|
||||||
|
4. 过期处理测试
|
||||||
|
|
||||||
|
### 安全测试
|
||||||
|
1. 令牌篡改测试
|
||||||
|
2. 过期令牌测试
|
||||||
|
3. 并发刷新测试
|
||||||
|
4. 版本不匹配测试
|
||||||
|
|
||||||
|
## 📞 后续支持
|
||||||
|
|
||||||
|
- 监控令牌刷新频率
|
||||||
|
- 分析用户登录模式
|
||||||
|
- 优化过期时间配置
|
||||||
|
- 收集用户反馈
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**重构完成时间**: 2025年11月1日
|
||||||
|
**文档版本**: v1.0
|
||||||
|
**兼容性**: 向后兼容,支持渐进式迁移
|
||||||
565
SOCKET_API.md
Normal file
565
SOCKET_API.md
Normal file
@ -0,0 +1,565 @@
|
|||||||
|
# Socket.IO 实时频道接口文档(前端)
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
ClassworksKV 提供基于 Socket.IO 的实时键值变更通知服务。前端使用 **KV token**(应用安装 token)加入频道,服务端会自动将 token 映射到对应设备的 uuid 房间。**同一设备的不同 token 会被归入同一频道**,因此多个客户端/应用可以共享实时更新。
|
||||||
|
|
||||||
|
**重要变更**:不再支持直接使用 uuid 加入频道,所有连接必须使用有效的 KV token。
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
前端项目安装 Socket.IO 客户端:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install socket.io-client
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm add socket.io-client
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn add socket.io-client
|
||||||
|
```
|
||||||
|
|
||||||
|
## 连接服务器
|
||||||
|
|
||||||
|
### 基础连接
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
|
||||||
|
const SERVER_URL = 'http://localhost:3000'; // 替换为实际服务器地址
|
||||||
|
|
||||||
|
const socket: Socket = io(SERVER_URL, {
|
||||||
|
transports: ['websocket'],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 连接时自动加入频道(推荐)
|
||||||
|
|
||||||
|
在连接握手时通过 query 参数传入 token,自动加入对应设备频道:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const socket = io(SERVER_URL, {
|
||||||
|
transports: ['websocket'],
|
||||||
|
query: {
|
||||||
|
token: '<your-kv-app-token>', // 或使用 apptoken 参数
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听加入成功
|
||||||
|
socket.on('joined', (info) => {
|
||||||
|
console.log('已加入频道:', info);
|
||||||
|
// { by: 'token', uuid: 'device-uuid-xxx' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听加入失败
|
||||||
|
socket.on('join-error', (error) => {
|
||||||
|
console.error('加入频道失败:', error);
|
||||||
|
// { by: 'token', reason: 'invalid_token' }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 事件接口
|
||||||
|
|
||||||
|
### 1. 客户端发送的事件
|
||||||
|
|
||||||
|
#### `join-token` - 使用 token 加入频道
|
||||||
|
|
||||||
|
连接后按需加入频道。
|
||||||
|
|
||||||
|
**载荷格式:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
token?: string; // KV token(二选一)
|
||||||
|
apptoken?: string; // 或使用 apptoken 字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```typescript
|
||||||
|
socket.emit('join-token', { token: '<your-kv-app-token>' });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `leave-token` - 使用 token 离开频道
|
||||||
|
|
||||||
|
离开指定 token 对应的设备频道。
|
||||||
|
|
||||||
|
**载荷格式:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
token?: string;
|
||||||
|
apptoken?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```typescript
|
||||||
|
socket.emit('leave-token', { token: '<your-kv-app-token>' });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `leave-all` - 离开所有频道
|
||||||
|
|
||||||
|
断开前清理,离开该连接加入的所有设备频道。
|
||||||
|
|
||||||
|
**载荷:** 无
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```typescript
|
||||||
|
socket.emit('leave-all');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 服务端发送的事件
|
||||||
|
|
||||||
|
#### `joined` - 加入成功通知
|
||||||
|
|
||||||
|
当成功加入频道后,服务端会发送此事件。
|
||||||
|
|
||||||
|
**载荷格式:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
by: 'token';
|
||||||
|
uuid: string; // 设备 uuid(用于调试/日志)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```typescript
|
||||||
|
socket.on('joined', (info) => {
|
||||||
|
console.log(`成功加入设备 ${info.uuid} 的频道`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `join-error` - 加入失败通知
|
||||||
|
|
||||||
|
token 无效或查询失败时触发。
|
||||||
|
|
||||||
|
**载荷格式:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
by: 'token';
|
||||||
|
reason: 'invalid_token'; // 失败原因
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```typescript
|
||||||
|
socket.on('join-error', (error) => {
|
||||||
|
console.error('Token 无效,无法加入频道');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `kv-key-changed` - 键值变更广播
|
||||||
|
|
||||||
|
当设备下的 KV 键被创建/更新/删除时,向该设备频道内所有连接广播此事件。
|
||||||
|
|
||||||
|
**载荷格式:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
uuid: string; // 设备 uuid
|
||||||
|
key: string; // 变更的键名
|
||||||
|
action: 'upsert' | 'delete'; // 操作类型
|
||||||
|
|
||||||
|
// 仅 action='upsert' 时存在:
|
||||||
|
created?: boolean; // 是否首次创建
|
||||||
|
updatedAt?: string; // 更新时间(ISO 8601)
|
||||||
|
batch?: boolean; // 是否为批量导入中的单条
|
||||||
|
|
||||||
|
// 仅 action='delete' 时存在:
|
||||||
|
deletedAt?: string; // 删除时间(ISO 8601)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```typescript
|
||||||
|
socket.on('kv-key-changed', (msg) => {
|
||||||
|
if (msg.action === 'upsert') {
|
||||||
|
console.log(`键 ${msg.key} 已${msg.created ? '创建' : '更新'}`);
|
||||||
|
// 刷新本地缓存或重新获取数据
|
||||||
|
} else if (msg.action === 'delete') {
|
||||||
|
console.log(`键 ${msg.key} 已删除`);
|
||||||
|
// 从本地缓存移除
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**载荷示例:**
|
||||||
|
|
||||||
|
- 新建/更新键:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "device-001",
|
||||||
|
"key": "settings/theme",
|
||||||
|
"action": "upsert",
|
||||||
|
"created": false,
|
||||||
|
"updatedAt": "2025-10-25T08:30:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 删除键:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "device-001",
|
||||||
|
"key": "settings/theme",
|
||||||
|
"action": "delete",
|
||||||
|
"deletedAt": "2025-10-25T08:35:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 批量导入中的单条:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uuid": "device-001",
|
||||||
|
"key": "config/version",
|
||||||
|
"action": "upsert",
|
||||||
|
"created": true,
|
||||||
|
"updatedAt": "2025-10-25T08:40:00.000Z",
|
||||||
|
"batch": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `device-joined` - 设备频道连接数变化(可选)
|
||||||
|
|
||||||
|
当有新连接加入某设备频道时广播,用于显示在线人数。
|
||||||
|
|
||||||
|
**载荷格式:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
uuid: string; // 设备 uuid
|
||||||
|
connections: number; // 当前连接数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```typescript
|
||||||
|
socket.on('device-joined', (info) => {
|
||||||
|
console.log(`设备 ${info.uuid} 当前有 ${info.connections} 个连接`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整使用示例
|
||||||
|
|
||||||
|
### React Hook 封装
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
|
||||||
|
const SERVER_URL = import.meta.env.VITE_SERVER_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
interface KvKeyChange {
|
||||||
|
uuid: string;
|
||||||
|
key: string;
|
||||||
|
action: 'upsert' | 'delete';
|
||||||
|
created?: boolean;
|
||||||
|
updatedAt?: string;
|
||||||
|
deletedAt?: string;
|
||||||
|
batch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKvChannel(
|
||||||
|
token: string | null,
|
||||||
|
onKeyChanged?: (event: KvKeyChange) => void
|
||||||
|
) {
|
||||||
|
const socketRef = useRef<Socket | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
// 创建连接并加入频道
|
||||||
|
const socket = io(SERVER_URL, {
|
||||||
|
transports: ['websocket'],
|
||||||
|
query: { token },
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('joined', (info) => {
|
||||||
|
console.log('已加入设备频道:', info.uuid);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('join-error', (err) => {
|
||||||
|
console.error('加入频道失败:', err.reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('kv-key-changed', (msg: KvKeyChange) => {
|
||||||
|
onKeyChanged?.(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current = socket;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.emit('leave-all');
|
||||||
|
socket.close();
|
||||||
|
};
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
return socketRef.current;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vue Composable 封装
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ref, watch, onUnmounted } from 'vue';
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
|
||||||
|
const SERVER_URL = import.meta.env.VITE_SERVER_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
export function useKvChannel(token: Ref<string | null>) {
|
||||||
|
const socket = ref<Socket | null>(null);
|
||||||
|
const isConnected = ref(false);
|
||||||
|
const deviceUuid = ref<string | null>(null);
|
||||||
|
|
||||||
|
watch(token, (newToken) => {
|
||||||
|
// 清理旧连接
|
||||||
|
if (socket.value) {
|
||||||
|
socket.value.emit('leave-all');
|
||||||
|
socket.value.close();
|
||||||
|
socket.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newToken) return;
|
||||||
|
|
||||||
|
// 创建新连接
|
||||||
|
const s = io(SERVER_URL, {
|
||||||
|
transports: ['websocket'],
|
||||||
|
query: { token: newToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
s.on('connect', () => {
|
||||||
|
isConnected.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
s.on('disconnect', () => {
|
||||||
|
isConnected.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
s.on('joined', (info) => {
|
||||||
|
deviceUuid.value = info.uuid;
|
||||||
|
console.log('已加入设备频道:', info.uuid);
|
||||||
|
});
|
||||||
|
|
||||||
|
s.on('join-error', (err) => {
|
||||||
|
console.error('加入失败:', err.reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value = s;
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (socket.value) {
|
||||||
|
socket.value.emit('leave-all');
|
||||||
|
socket.value.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { socket, isConnected, deviceUuid };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用示例(React)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useKvChannel } from './hooks/useKvChannel';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const token = localStorage.getItem('kv-token');
|
||||||
|
|
||||||
|
useKvChannel(token, (event) => {
|
||||||
|
console.log('KV 变更:', event);
|
||||||
|
|
||||||
|
if (event.action === 'upsert') {
|
||||||
|
// 更新本地状态或重新获取数据
|
||||||
|
fetchKeyValue(event.key);
|
||||||
|
} else if (event.action === 'delete') {
|
||||||
|
// 从本地移除
|
||||||
|
removeFromCache(event.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>实时监听中...</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REST API:查询在线设备
|
||||||
|
|
||||||
|
除了 Socket.IO 实时事件,还提供 HTTP 接口查询当前在线设备列表。
|
||||||
|
|
||||||
|
### `GET /devices/online`
|
||||||
|
|
||||||
|
**响应格式:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: true;
|
||||||
|
devices: Array<{
|
||||||
|
uuid: string; // 设备 uuid
|
||||||
|
connections: number; // 当前连接数
|
||||||
|
name: string | null; // 设备名称(若已设置)
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```typescript
|
||||||
|
const response = await fetch(`${SERVER_URL}/devices/online`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('在线设备:', data.devices);
|
||||||
|
// [{ uuid: 'device-001', connections: 3, name: 'My Device' }, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 获取 KV Token
|
||||||
|
|
||||||
|
前端需要先获取有效的 KV token 才能加入频道。Token 通过以下接口获取:
|
||||||
|
|
||||||
|
### 安装应用获取 token
|
||||||
|
|
||||||
|
**接口:** `POST /apps/devices/:uuid/install/:appId`
|
||||||
|
|
||||||
|
**认证:** 需要设备 UUID 认证(密码或账户 JWT)
|
||||||
|
|
||||||
|
**响应包含:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
token: string; // 用于 KV 操作和加入频道
|
||||||
|
note: string | null;
|
||||||
|
name: string | null; // 等同于 note,便于展示
|
||||||
|
installedAt: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 列出设备已有的 token
|
||||||
|
|
||||||
|
**接口:** `GET /apps/tokens?uuid=<device-uuid>`
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: true;
|
||||||
|
tokens: Array<{
|
||||||
|
id: string;
|
||||||
|
token: string;
|
||||||
|
appId: string;
|
||||||
|
installedAt: string;
|
||||||
|
note: string | null;
|
||||||
|
name: string | null; // 等同于 note
|
||||||
|
}>;
|
||||||
|
deviceUuid: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项与最佳实践
|
||||||
|
|
||||||
|
1. **Token 必需**:所有连接必须提供有效的 KV token,不再支持直接使用 uuid。
|
||||||
|
|
||||||
|
2. **频道归并**:同一设备的不同 token 会自动归入同一房间(以设备 uuid 为房间名),因此多个应用/客户端可以共享实时更新。
|
||||||
|
|
||||||
|
3. **连接管理**:
|
||||||
|
- 组件卸载时调用 `leave-all` 或 `leave-token` 清理连接
|
||||||
|
- 避免频繁创建/销毁连接,建议在应用全局维护单个 socket 实例
|
||||||
|
|
||||||
|
4. **重连处理**:
|
||||||
|
- Socket.IO 客户端内置自动重连
|
||||||
|
- 在 `connect` 事件后重新 emit `join-token` 确保重连后仍在频道内(或在握手时传 token 自动加入)
|
||||||
|
|
||||||
|
5. **CORS 配置**:
|
||||||
|
- 服务端通过环境变量 `FRONTEND_URL` 控制允许的来源
|
||||||
|
- 未设置时默认为 `*`(允许所有来源)
|
||||||
|
- 生产环境建议设置为前端实际域名
|
||||||
|
|
||||||
|
6. **错误处理**:
|
||||||
|
- 监听 `join-error` 事件处理 token 无效情况
|
||||||
|
- 监听 `connect_error` 处理网络连接失败
|
||||||
|
|
||||||
|
7. **性能优化**:
|
||||||
|
- 批量导入时会逐条广播,前端可根据 `batch: true` 标记做去抖处理
|
||||||
|
- 建议在本地维护 KV 缓存,收到变更通知时增量更新而非全量刷新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境变量配置
|
||||||
|
|
||||||
|
服务端需要配置以下环境变量:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Socket.IO CORS 允许的来源
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
|
||||||
|
# 服务器端口(可选,默认 3000)
|
||||||
|
PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 如何支持多个设备?
|
||||||
|
|
||||||
|
A: 对每个设备的 token 分别调用 `join-token`,或在连接时传入一个 token,后续通过事件加入其他设备。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
socket.emit('join-token', { token: token1 });
|
||||||
|
socket.emit('join-token', { token: token2 });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: 广播延迟有多大?
|
||||||
|
|
||||||
|
A: 通常在毫秒级,取决于网络状况。WebSocket 连接建立后,广播几乎实时。
|
||||||
|
|
||||||
|
### Q: Token 过期怎么办?
|
||||||
|
|
||||||
|
A: Token 本身不会过期,除非手动删除应用安装记录。如收到 `join-error`,检查 token 是否已被卸载。
|
||||||
|
|
||||||
|
### Q: 可以在 Node.js 后端使用吗?
|
||||||
|
|
||||||
|
A: 可以,使用相同的 socket.io-client 包,接口完全一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.1.0 (2025-10-25)
|
||||||
|
|
||||||
|
**破坏性变更:**
|
||||||
|
- 移除直接使用 uuid 加入频道的接口(`join-device` / `leave-device`)
|
||||||
|
- 现在必须使用 KV token 通过 `join-token` 或握手 query 加入
|
||||||
|
|
||||||
|
**新增:**
|
||||||
|
- `leave-all` 事件:离开所有已加入的频道
|
||||||
|
- 握手时支持 `token` 和 `apptoken` 两种参数名
|
||||||
|
|
||||||
|
**改进:**
|
||||||
|
- 同一设备的不同 token 自动归入同一房间
|
||||||
|
- 优化在线设备计数准确性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术支持
|
||||||
|
|
||||||
|
如有问题,请查阅:
|
||||||
|
- 服务端源码:`utils/socket.js`
|
||||||
|
- KV 路由:`routes/kv-token.js`
|
||||||
|
- 设备管理:`routes/device.js`
|
||||||
|
|
||||||
|
或提交 Issue 到项目仓库。
|
||||||
@ -18,9 +18,9 @@ export const oauthProviders = {
|
|||||||
zerocat: {
|
zerocat: {
|
||||||
clientId: process.env.ZEROCAT_CLIENT_ID,
|
clientId: process.env.ZEROCAT_CLIENT_ID,
|
||||||
clientSecret: process.env.ZEROCAT_CLIENT_SECRET,
|
clientSecret: process.env.ZEROCAT_CLIENT_SECRET,
|
||||||
authorizationURL: "https://api.zcservice.houlang.cloud/oauth/authorize",
|
authorizationURL: "https://zerocat-api.houlang.cloud/oauth/authorize",
|
||||||
tokenURL: "https://api.zcservice.houlang.cloud/oauth/token",
|
tokenURL: "https://zerocat-api.houlang.cloud/oauth/token",
|
||||||
userInfoURL: "https://api.zcservice.houlang.cloud/oauth/userinfo",
|
userInfoURL: "https://zerocat-api.houlang.cloud/oauth/userinfo",
|
||||||
scope: "user:basic user:email",
|
scope: "user:basic user:email",
|
||||||
// 展示相关
|
// 展示相关
|
||||||
name: "ZeroCat",
|
name: "ZeroCat",
|
||||||
|
|||||||
91
docker/schema.prisma
Normal file
91
docker/schema.prisma
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client"
|
||||||
|
output = "../generated/prisma"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
}
|
||||||
|
|
||||||
|
model KVStore {
|
||||||
|
deviceId Int
|
||||||
|
key String
|
||||||
|
value Json
|
||||||
|
creatorIp String? @default("")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// 关联关系
|
||||||
|
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([deviceId, key])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Account {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
provider String // OAuth提供者 (例如: google, github, gitlab等)
|
||||||
|
providerId String // 提供者返回的用户唯一ID
|
||||||
|
email String? // 用户邮箱
|
||||||
|
name String? // 用户名称
|
||||||
|
avatarUrl String? // 用户头像URL
|
||||||
|
providerData Json? // OAuth提供者返回的完整信息
|
||||||
|
accessToken String? @db.Text // 账户访问令牌
|
||||||
|
refreshToken String? @db.Text // 刷新令牌
|
||||||
|
refreshTokenExpiry DateTime? // 刷新令牌过期时间
|
||||||
|
tokenVersion Int @default(1) // 令牌版本,用于令牌失效
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// 关联的设备
|
||||||
|
devices Device[]
|
||||||
|
|
||||||
|
@@unique([provider, providerId]) // 确保同一提供者的用户ID唯一
|
||||||
|
}
|
||||||
|
|
||||||
|
model Device {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
uuid String @unique // 设备的唯一标识符
|
||||||
|
name String?
|
||||||
|
accountId String? // 关联的账户ID
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
password String?
|
||||||
|
passwordHint String?
|
||||||
|
namespace String? @unique // 用户自定义的唯一命名空间
|
||||||
|
|
||||||
|
// 关联关系
|
||||||
|
account Account? @relation(fields: [accountId], references: [id], onDelete: SetNull)
|
||||||
|
appInstalls AppInstall[]
|
||||||
|
kvStore KVStore[] // 设备相关的KV存储
|
||||||
|
autoAuths AutoAuth[] // 自动授权配置
|
||||||
|
}
|
||||||
|
|
||||||
|
model AppInstall {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
deviceId Int // 关联的设备ID
|
||||||
|
appId String // 应用ID (SHA256 hash)
|
||||||
|
token String @unique // 应用安装的唯一访问令牌,拥有完整KV读写权限
|
||||||
|
note String? // 安装备注
|
||||||
|
isReadOnly Boolean @default(false) // 是否只读
|
||||||
|
deviceType String? // 设备类型: teacher(教师), student(学生), classroom(班级一体机), parent(家长)
|
||||||
|
installedAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// 关联关系
|
||||||
|
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model AutoAuth {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
deviceId Int // 关联的设备ID
|
||||||
|
password String? // 配置密码,可以为空
|
||||||
|
deviceType String? // 自动设备类型: teacher(教师), student(学生), classroom(班级一体机), parent(家长)
|
||||||
|
isReadOnly Boolean @default(false) // 是否只读
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// 关联关系
|
||||||
|
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([deviceId, password]) // 同一设备的密码必须唯一
|
||||||
|
}
|
||||||
44
generated/prisma/browser.ts
Normal file
44
generated/prisma/browser.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||||
|
* Use it to get access to models, enums, and input types.
|
||||||
|
*
|
||||||
|
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||||
|
* See `client.ts` for the standard, server-side entry point.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
||||||
|
export { Prisma }
|
||||||
|
export * as $Enums from './enums.ts'
|
||||||
|
export * from './enums.ts';
|
||||||
|
/**
|
||||||
|
* Model Account
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Account = Prisma.AccountModel
|
||||||
|
/**
|
||||||
|
* Model AppInstall
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type AppInstall = Prisma.AppInstallModel
|
||||||
|
/**
|
||||||
|
* Model AutoAuth
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type AutoAuth = Prisma.AutoAuthModel
|
||||||
|
/**
|
||||||
|
* Model Device
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Device = Prisma.DeviceModel
|
||||||
|
/**
|
||||||
|
* Model KVStore
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type KVStore = Prisma.KVStoreModel
|
||||||
66
generated/prisma/client.ts
Normal file
66
generated/prisma/client.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||||
|
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as process from 'node:process'
|
||||||
|
import * as path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
import * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import * as $Enums from "./enums.ts"
|
||||||
|
import * as $Class from "./internal/class.ts"
|
||||||
|
import * as Prisma from "./internal/prismaNamespace.ts"
|
||||||
|
|
||||||
|
export * as $Enums from './enums.ts'
|
||||||
|
export * from "./enums.ts"
|
||||||
|
/**
|
||||||
|
* ## Prisma Client
|
||||||
|
*
|
||||||
|
* Type-safe database client for TypeScript
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient()
|
||||||
|
* // Fetch zero or more Accounts
|
||||||
|
* const accounts = await prisma.account.findMany()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/client).
|
||||||
|
*/
|
||||||
|
export const PrismaClient = $Class.getPrismaClientClass()
|
||||||
|
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||||
|
export { Prisma }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model Account
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Account = Prisma.AccountModel
|
||||||
|
/**
|
||||||
|
* Model AppInstall
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type AppInstall = Prisma.AppInstallModel
|
||||||
|
/**
|
||||||
|
* Model AutoAuth
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type AutoAuth = Prisma.AutoAuthModel
|
||||||
|
/**
|
||||||
|
* Model Device
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Device = Prisma.DeviceModel
|
||||||
|
/**
|
||||||
|
* Model KVStore
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type KVStore = Prisma.KVStoreModel
|
||||||
502
generated/prisma/commonInputTypes.ts
Normal file
502
generated/prisma/commonInputTypes.ts
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import * as $Enums from "./enums.ts"
|
||||||
|
import type * as Prisma from "./internal/prismaNamespace.ts"
|
||||||
|
|
||||||
|
|
||||||
|
export type StringFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
mode?: Prisma.QueryMode
|
||||||
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
mode?: Prisma.QueryMode
|
||||||
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JsonNullableFilter<$PrismaModel = never> =
|
||||||
|
| Prisma.PatchUndefined<
|
||||||
|
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
Required<JsonNullableFilterBase<$PrismaModel>>
|
||||||
|
>
|
||||||
|
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
|
||||||
|
|
||||||
|
export type JsonNullableFilterBase<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
path?: string[]
|
||||||
|
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||||
|
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IntFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SortOrderInput = {
|
||||||
|
sort: Prisma.SortOrder
|
||||||
|
nulls?: Prisma.NullsOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
mode?: Prisma.QueryMode
|
||||||
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
mode?: Prisma.QueryMode
|
||||||
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
|
||||||
|
| Prisma.PatchUndefined<
|
||||||
|
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
|
||||||
|
>
|
||||||
|
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
|
||||||
|
|
||||||
|
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
path?: string[]
|
||||||
|
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||||
|
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoolFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JsonFilter<$PrismaModel = never> =
|
||||||
|
| Prisma.PatchUndefined<
|
||||||
|
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
Required<JsonFilterBase<$PrismaModel>>
|
||||||
|
>
|
||||||
|
| Prisma.OptionalFlat<Omit<Required<JsonFilterBase<$PrismaModel>>, 'path'>>
|
||||||
|
|
||||||
|
export type JsonFilterBase<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
path?: string[]
|
||||||
|
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||||
|
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JsonWithAggregatesFilter<$PrismaModel = never> =
|
||||||
|
| Prisma.PatchUndefined<
|
||||||
|
Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
Required<JsonWithAggregatesFilterBase<$PrismaModel>>
|
||||||
|
>
|
||||||
|
| Prisma.OptionalFlat<Omit<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
|
||||||
|
|
||||||
|
export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
path?: string[]
|
||||||
|
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||||
|
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedJsonFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedJsonFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedJsonNullableFilter<$PrismaModel = never> =
|
||||||
|
| Prisma.PatchUndefined<
|
||||||
|
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
Required<NestedJsonNullableFilterBase<$PrismaModel>>
|
||||||
|
>
|
||||||
|
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
|
||||||
|
|
||||||
|
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
path?: string[]
|
||||||
|
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||||
|
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
|
||||||
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
|
||||||
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedJsonFilter<$PrismaModel = never> =
|
||||||
|
| Prisma.PatchUndefined<
|
||||||
|
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
Required<NestedJsonFilterBase<$PrismaModel>>
|
||||||
|
>
|
||||||
|
| Prisma.OptionalFlat<Omit<Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>
|
||||||
|
|
||||||
|
export type NestedJsonFilterBase<$PrismaModel = never> = {
|
||||||
|
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
path?: string[]
|
||||||
|
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
|
||||||
|
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
|
||||||
|
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
|
||||||
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
15
generated/prisma/enums.ts
Normal file
15
generated/prisma/enums.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file exports all enum related types from the schema.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// This file is empty because there are no enums in the schema.
|
||||||
|
export {}
|
||||||
232
generated/prisma/internal/class.ts
Normal file
232
generated/prisma/internal/class.ts
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* WARNING: This is an internal file that is subject to change!
|
||||||
|
*
|
||||||
|
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||||
|
*
|
||||||
|
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import type * as Prisma from "./prismaNamespace.ts"
|
||||||
|
|
||||||
|
|
||||||
|
const config: runtime.GetPrismaClientConfig = {
|
||||||
|
"previewFeatures": [],
|
||||||
|
"clientVersion": "7.3.0",
|
||||||
|
"engineVersion": "9d6ad21cbbceab97458517b147a6a09ff43aa735",
|
||||||
|
"activeProvider": "postgresql",
|
||||||
|
"inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel Account {\n id String @id(map: \"idx_18303_PRIMARY\") @db.VarChar(191)\n provider String @db.VarChar(191)\n providerId String @db.VarChar(191)\n email String? @db.VarChar(191)\n name String? @db.VarChar(191)\n avatarUrl String? @db.VarChar(191)\n providerData Json? @db.Json\n accessToken String?\n createdAt DateTime @default(now()) @db.Timestamptz(6)\n updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)\n refreshToken String?\n refreshTokenExpiry DateTime? @db.Timestamptz(6)\n tokenVersion Int @default(1)\n\n devices Device[]\n\n @@unique([provider, providerId], map: \"idx_18303_Account_provider_providerId_key\")\n}\n\nmodel AppInstall {\n id String @id(map: \"idx_18310_PRIMARY\") @db.VarChar(191)\n deviceId Int\n appId String @db.VarChar(191)\n token String @unique(map: \"idx_18310_AppInstall_token_key\") @db.VarChar(191)\n note String? @db.VarChar(191)\n installedAt DateTime @default(now()) @db.Timestamptz(6)\n updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)\n deviceType String? @db.VarChar(191)\n isReadOnly Boolean @default(false)\n\n device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)\n\n @@index([deviceId], map: \"idx_18310_AppInstall_deviceId_fkey\")\n}\n\nmodel AutoAuth {\n id String @id(map: \"idx_18317_PRIMARY\") @db.VarChar(191)\n deviceId Int\n password String? @db.VarChar(191)\n deviceType String? @db.VarChar(191)\n isReadOnly Boolean @default(false)\n createdAt DateTime @default(now()) @db.Timestamptz(6)\n updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)\n\n device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)\n\n @@unique([deviceId, password], map: \"idx_18317_AutoAuth_deviceId_password_key\")\n}\n\nmodel Device {\n id Int @id(map: \"idx_18324_PRIMARY\")\n uuid String @unique(map: \"idx_18324_Device_uuid_key\") @db.VarChar(191)\n name String? @db.VarChar(191)\n accountId String? @db.VarChar(191)\n createdAt DateTime @default(now()) @db.Timestamptz(6)\n updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)\n password String? @db.VarChar(191)\n passwordHint String? @db.VarChar(191)\n namespace String? @unique(map: \"idx_18324_Device_namespace_key\") @db.VarChar(191)\n\n // 关联关系\n account Account? @relation(fields: [accountId], references: [id], onDelete: SetNull)\n appInstalls AppInstall[]\n kvStore KVStore[] // 设备相关的KV存储\n autoAuths AutoAuth[] // 自动授权配置\n\n @@index([accountId], map: \"idx_18324_Device_accountId_fkey\")\n}\n\nmodel KVStore {\n deviceId Int\n key String @db.VarChar(191)\n value Json @db.Json\n creatorIp String? @default(\"\") @db.VarChar(191)\n createdAt DateTime @default(now()) @db.Timestamptz(6)\n updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)\n\n device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)\n\n @@id([deviceId, key], map: \"idx_18330_PRIMARY\")\n}\n",
|
||||||
|
"runtimeDataModel": {
|
||||||
|
"models": {},
|
||||||
|
"enums": {},
|
||||||
|
"types": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.runtimeDataModel = JSON.parse("{\"models\":{\"Account\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"provider\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"avatarUrl\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"accessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshTokenExpiry\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"tokenVersion\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"devices\",\"kind\":\"object\",\"type\":\"Device\",\"relationName\":\"AccountToDevice\"}],\"dbName\":null},\"AppInstall\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deviceId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"note\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"installedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"deviceType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isReadOnly\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"device\",\"kind\":\"object\",\"type\":\"Device\",\"relationName\":\"AppInstallToDevice\"}],\"dbName\":null},\"AutoAuth\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deviceId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deviceType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isReadOnly\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"device\",\"kind\":\"object\",\"type\":\"Device\",\"relationName\":\"AutoAuthToDevice\"}],\"dbName\":null},\"Device\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"uuid\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"passwordHint\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"namespace\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToDevice\"},{\"name\":\"appInstalls\",\"kind\":\"object\",\"type\":\"AppInstall\",\"relationName\":\"AppInstallToDevice\"},{\"name\":\"kvStore\",\"kind\":\"object\",\"type\":\"KVStore\",\"relationName\":\"DeviceToKVStore\"},{\"name\":\"autoAuths\",\"kind\":\"object\",\"type\":\"AutoAuth\",\"relationName\":\"AutoAuthToDevice\"}],\"dbName\":null},\"KVStore\":{\"fields\":[{\"name\":\"deviceId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"creatorIp\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"device\",\"kind\":\"object\",\"type\":\"Device\",\"relationName\":\"DeviceToKVStore\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
||||||
|
|
||||||
|
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
||||||
|
const { Buffer } = await import('node:buffer')
|
||||||
|
const wasmArray = Buffer.from(wasmBase64, 'base64')
|
||||||
|
return new WebAssembly.Module(wasmArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.compilerWasm = {
|
||||||
|
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.mjs"),
|
||||||
|
|
||||||
|
getQueryCompilerWasmModule: async () => {
|
||||||
|
const { wasm } = await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.wasm-base64.mjs")
|
||||||
|
return await decodeBase64AsWasm(wasm)
|
||||||
|
},
|
||||||
|
|
||||||
|
importName: "./query_compiler_fast_bg.js"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
||||||
|
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
|
||||||
|
|
||||||
|
export interface PrismaClientConstructor {
|
||||||
|
/**
|
||||||
|
* ## Prisma Client
|
||||||
|
*
|
||||||
|
* Type-safe database client for TypeScript
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient()
|
||||||
|
* // Fetch zero or more Accounts
|
||||||
|
* const accounts = await prisma.account.findMany()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/client).
|
||||||
|
*/
|
||||||
|
|
||||||
|
new <
|
||||||
|
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||||
|
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
||||||
|
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
||||||
|
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||||
|
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ## Prisma Client
|
||||||
|
*
|
||||||
|
* Type-safe database client for TypeScript
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const prisma = new PrismaClient()
|
||||||
|
* // Fetch zero or more Accounts
|
||||||
|
* const accounts = await prisma.account.findMany()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/client).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface PrismaClient<
|
||||||
|
in LogOpts extends Prisma.LogLevel = never,
|
||||||
|
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
||||||
|
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||||
|
> {
|
||||||
|
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
|
||||||
|
|
||||||
|
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect with the database
|
||||||
|
*/
|
||||||
|
$connect(): runtime.Types.Utils.JsPromise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the database
|
||||||
|
*/
|
||||||
|
$disconnect(): runtime.Types.Utils.JsPromise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a prepared raw query and returns the number of affected rows.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||||
|
*/
|
||||||
|
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a raw query and returns the number of affected rows.
|
||||||
|
* Susceptible to SQL injections, see documentation.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||||
|
*/
|
||||||
|
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a prepared raw query and returns the `SELECT` data.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||||
|
*/
|
||||||
|
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a raw query and returns the `SELECT` data.
|
||||||
|
* Susceptible to SQL injections, see documentation.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||||
|
*/
|
||||||
|
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* const [george, bob, alice] = await prisma.$transaction([
|
||||||
|
* prisma.user.create({ data: { name: 'George' } }),
|
||||||
|
* prisma.user.create({ data: { name: 'Bob' } }),
|
||||||
|
* prisma.user.create({ data: { name: 'Alice' } }),
|
||||||
|
* ])
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
||||||
|
*/
|
||||||
|
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
||||||
|
|
||||||
|
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
|
||||||
|
|
||||||
|
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
|
||||||
|
extArgs: ExtArgs
|
||||||
|
}>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `prisma.account`: Exposes CRUD operations for the **Account** model.
|
||||||
|
* Example usage:
|
||||||
|
* ```ts
|
||||||
|
* // Fetch zero or more Accounts
|
||||||
|
* const accounts = await prisma.account.findMany()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get account(): Prisma.AccountDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `prisma.appInstall`: Exposes CRUD operations for the **AppInstall** model.
|
||||||
|
* Example usage:
|
||||||
|
* ```ts
|
||||||
|
* // Fetch zero or more AppInstalls
|
||||||
|
* const appInstalls = await prisma.appInstall.findMany()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get appInstall(): Prisma.AppInstallDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `prisma.autoAuth`: Exposes CRUD operations for the **AutoAuth** model.
|
||||||
|
* Example usage:
|
||||||
|
* ```ts
|
||||||
|
* // Fetch zero or more AutoAuths
|
||||||
|
* const autoAuths = await prisma.autoAuth.findMany()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get autoAuth(): Prisma.AutoAuthDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `prisma.device`: Exposes CRUD operations for the **Device** model.
|
||||||
|
* Example usage:
|
||||||
|
* ```ts
|
||||||
|
* // Fetch zero or more Devices
|
||||||
|
* const devices = await prisma.device.findMany()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get device(): Prisma.DeviceDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `prisma.kVStore`: Exposes CRUD operations for the **KVStore** model.
|
||||||
|
* Example usage:
|
||||||
|
* ```ts
|
||||||
|
* // Fetch zero or more KVStores
|
||||||
|
* const kVStores = await prisma.kVStore.findMany()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get kVStore(): Prisma.KVStoreDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPrismaClientClass(): PrismaClientConstructor {
|
||||||
|
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
|
||||||
|
}
|
||||||
1186
generated/prisma/internal/prismaNamespace.ts
Normal file
1186
generated/prisma/internal/prismaNamespace.ts
Normal file
File diff suppressed because it is too large
Load Diff
197
generated/prisma/internal/prismaNamespaceBrowser.ts
Normal file
197
generated/prisma/internal/prismaNamespaceBrowser.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* WARNING: This is an internal file that is subject to change!
|
||||||
|
*
|
||||||
|
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||||
|
*
|
||||||
|
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
|
||||||
|
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||||
|
*
|
||||||
|
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||||
|
* model files in the `model` directory!
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||||
|
|
||||||
|
export type * from '../models.ts'
|
||||||
|
export type * from './prismaNamespace.ts'
|
||||||
|
|
||||||
|
export const Decimal = runtime.Decimal
|
||||||
|
|
||||||
|
|
||||||
|
export const NullTypes = {
|
||||||
|
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||||
|
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||||
|
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const DbNull = runtime.DbNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const JsonNull = runtime.JsonNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||||
|
*
|
||||||
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
|
*/
|
||||||
|
export const AnyNull = runtime.AnyNull
|
||||||
|
|
||||||
|
|
||||||
|
export const ModelName = {
|
||||||
|
Account: 'Account',
|
||||||
|
AppInstall: 'AppInstall',
|
||||||
|
AutoAuth: 'AutoAuth',
|
||||||
|
Device: 'Device',
|
||||||
|
KVStore: 'KVStore'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
} as const)
|
||||||
|
|
||||||
|
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||||
|
|
||||||
|
|
||||||
|
export const AccountScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
provider: 'provider',
|
||||||
|
providerId: 'providerId',
|
||||||
|
email: 'email',
|
||||||
|
name: 'name',
|
||||||
|
avatarUrl: 'avatarUrl',
|
||||||
|
providerData: 'providerData',
|
||||||
|
accessToken: 'accessToken',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt',
|
||||||
|
refreshToken: 'refreshToken',
|
||||||
|
refreshTokenExpiry: 'refreshTokenExpiry',
|
||||||
|
tokenVersion: 'tokenVersion'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type AccountScalarFieldEnum = (typeof AccountScalarFieldEnum)[keyof typeof AccountScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const AppInstallScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
deviceId: 'deviceId',
|
||||||
|
appId: 'appId',
|
||||||
|
token: 'token',
|
||||||
|
note: 'note',
|
||||||
|
installedAt: 'installedAt',
|
||||||
|
updatedAt: 'updatedAt',
|
||||||
|
deviceType: 'deviceType',
|
||||||
|
isReadOnly: 'isReadOnly'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type AppInstallScalarFieldEnum = (typeof AppInstallScalarFieldEnum)[keyof typeof AppInstallScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const AutoAuthScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
deviceId: 'deviceId',
|
||||||
|
password: 'password',
|
||||||
|
deviceType: 'deviceType',
|
||||||
|
isReadOnly: 'isReadOnly',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type AutoAuthScalarFieldEnum = (typeof AutoAuthScalarFieldEnum)[keyof typeof AutoAuthScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const DeviceScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
uuid: 'uuid',
|
||||||
|
name: 'name',
|
||||||
|
accountId: 'accountId',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt',
|
||||||
|
password: 'password',
|
||||||
|
passwordHint: 'passwordHint',
|
||||||
|
namespace: 'namespace'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type DeviceScalarFieldEnum = (typeof DeviceScalarFieldEnum)[keyof typeof DeviceScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const KVStoreScalarFieldEnum = {
|
||||||
|
deviceId: 'deviceId',
|
||||||
|
key: 'key',
|
||||||
|
value: 'value',
|
||||||
|
creatorIp: 'creatorIp',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type KVStoreScalarFieldEnum = (typeof KVStoreScalarFieldEnum)[keyof typeof KVStoreScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const SortOrder = {
|
||||||
|
asc: 'asc',
|
||||||
|
desc: 'desc'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||||
|
|
||||||
|
|
||||||
|
export const NullableJsonNullValueInput = {
|
||||||
|
DbNull: DbNull,
|
||||||
|
JsonNull: JsonNull
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
|
||||||
|
|
||||||
|
|
||||||
|
export const JsonNullValueInput = {
|
||||||
|
JsonNull: JsonNull
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
|
||||||
|
|
||||||
|
|
||||||
|
export const QueryMode = {
|
||||||
|
default: 'default',
|
||||||
|
insensitive: 'insensitive'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
|
||||||
|
|
||||||
|
|
||||||
|
export const JsonNullValueFilter = {
|
||||||
|
DbNull: DbNull,
|
||||||
|
JsonNull: JsonNull,
|
||||||
|
AnyNull: AnyNull
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
|
||||||
|
|
||||||
|
|
||||||
|
export const NullsOrder = {
|
||||||
|
first: 'first',
|
||||||
|
last: 'last'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||||
|
|
||||||
16
generated/prisma/models.ts
Normal file
16
generated/prisma/models.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This is a barrel export file for all models and their related types.
|
||||||
|
*
|
||||||
|
* 🟢 You can import this file directly.
|
||||||
|
*/
|
||||||
|
export type * from './models/Account.ts'
|
||||||
|
export type * from './models/AppInstall.ts'
|
||||||
|
export type * from './models/AutoAuth.ts'
|
||||||
|
export type * from './models/Device.ts'
|
||||||
|
export type * from './models/KVStore.ts'
|
||||||
|
export type * from './commonInputTypes.ts'
|
||||||
1648
generated/prisma/models/Account.ts
Normal file
1648
generated/prisma/models/Account.ts
Normal file
File diff suppressed because it is too large
Load Diff
1528
generated/prisma/models/AppInstall.ts
Normal file
1528
generated/prisma/models/AppInstall.ts
Normal file
File diff suppressed because it is too large
Load Diff
1460
generated/prisma/models/AutoAuth.ts
Normal file
1460
generated/prisma/models/AutoAuth.ts
Normal file
File diff suppressed because it is too large
Load Diff
1972
generated/prisma/models/Device.ts
Normal file
1972
generated/prisma/models/Device.ts
Normal file
File diff suppressed because it is too large
Load Diff
1419
generated/prisma/models/KVStore.ts
Normal file
1419
generated/prisma/models/KVStore.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,25 +6,24 @@
|
|||||||
* 2. deviceInfoMiddleware - 仅获取设备信息,不创建新设备
|
* 2. deviceInfoMiddleware - 仅获取设备信息,不创建新设备
|
||||||
* 3. passwordMiddleware - 验证设备密码
|
* 3. passwordMiddleware - 验证设备密码
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {prisma} from "../utils/prisma.js";
|
|
||||||
import errors from "../utils/errors.js";
|
import errors from "../utils/errors.js";
|
||||||
import {verifyDevicePassword} from "../utils/crypto.js";
|
import {verifyDevicePassword} from "../utils/crypto.js";
|
||||||
import {analyzeDevice} from "../utils/deviceDetector.js";
|
import {analyzeDevice} from "../utils/deviceDetector.js";
|
||||||
|
import { prisma } from "../utils/prisma.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为新设备创建默认的自动登录配置
|
* 为新设备创建默认的自动登录配置
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
*/
|
*/
|
||||||
async function createDefaultAutoAuth(deviceid) {
|
async function createDefaultAutoAuth(deviceId) {
|
||||||
try {
|
try {
|
||||||
// 创建默认的自动授权配置:不需要密码、类型是classroom(一体机)
|
// 创建默认的自动授权配置:不需要密码、类型是classroom(一体机)
|
||||||
await prisma.autoAuth.create({
|
await prisma.autoAuth.create({
|
||||||
data: {
|
data: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
password: null, // 无密码
|
password: null, // 无密码
|
||||||
devicetype: "classroom", // 一体机类型
|
deviceType: "classroom", // 一体机类型
|
||||||
isreadonly: false, // 非只读
|
isReadOnly: false, // 非只读
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -59,7 +58,7 @@ export const deviceMiddleware = errors.catchAsync(async (req, res, next) => {
|
|||||||
if (!device) {
|
if (!device) {
|
||||||
// 设备不存在,自动创建并生成智能设备名称
|
// 设备不存在,自动创建并生成智能设备名称
|
||||||
const userAgent = req.headers['user-agent'];
|
const userAgent = req.headers['user-agent'];
|
||||||
const customDeviceType = req.body.devicetype || req.query.devicetype;
|
const customDeviceType = req.body.deviceType || req.query.deviceType;
|
||||||
const note = req.body.note || req.query.note;
|
const note = req.body.note || req.query.note;
|
||||||
|
|
||||||
// 生成设备名称,确保不为空
|
// 生成设备名称,确保不为空
|
||||||
@ -70,7 +69,7 @@ export const deviceMiddleware = errors.catchAsync(async (req, res, next) => {
|
|||||||
uuid: deviceUuid,
|
uuid: deviceUuid,
|
||||||
name: deviceName,
|
name: deviceName,
|
||||||
password: null,
|
password: null,
|
||||||
passwordhint: null,
|
passwordHint: null,
|
||||||
accountId: null,
|
accountId: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
import {generateAccessToken, validateAccountToken, verifyAccessToken} from "../utils/tokenManager.js";
|
import {generateAccessToken, validateAccountToken, verifyAccessToken} from "../utils/tokenManager.js";
|
||||||
import {verifyToken} from "../utils/jwt.js";
|
import {verifyToken} from "../utils/jwt.js";
|
||||||
import {prisma} from "../utils/prisma.js";
|
|
||||||
import errors from "../utils/errors.js";
|
import errors from "../utils/errors.js";
|
||||||
|
import { prisma } from "../utils/prisma.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新的JWT认证中间件(支持refresh token系统)
|
* 新的JWT认证中间件(支持refresh token系统)
|
||||||
|
|||||||
@ -4,9 +4,8 @@
|
|||||||
* 仅验证app token,设置设备和应用信息到res.locals
|
* 仅验证app token,设置设备和应用信息到res.locals
|
||||||
* 适用于所有KV相关的接口
|
* 适用于所有KV相关的接口
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {prisma} from "../utils/prisma.js";
|
|
||||||
import errors from "../utils/errors.js";
|
import errors from "../utils/errors.js";
|
||||||
|
import { prisma } from "../utils/prisma.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KV Token认证中间件
|
* KV Token认证中间件
|
||||||
@ -22,7 +21,7 @@ export const kvTokenAuth = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查找token对应的应用安装信息
|
// 查找token对应的应用安装信息
|
||||||
const appInstall = await prisma.appinstall.findUnique({
|
const appInstall = await prisma.appInstall.findUnique({
|
||||||
where: {token},
|
where: {token},
|
||||||
include: {
|
include: {
|
||||||
device: true,
|
device: true,
|
||||||
@ -36,7 +35,7 @@ export const kvTokenAuth = async (req, res, next) => {
|
|||||||
// 将信息存储到res.locals供后续使用
|
// 将信息存储到res.locals供后续使用
|
||||||
res.locals.device = appInstall.device;
|
res.locals.device = appInstall.device;
|
||||||
res.locals.appInstall = appInstall;
|
res.locals.appInstall = appInstall;
|
||||||
res.locals.deviceid = appInstall.device.id;
|
res.locals.deviceId = appInstall.device.id;
|
||||||
res.locals.token = token;
|
res.locals.token = token;
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -5,11 +5,10 @@
|
|||||||
* 2. 验证密码或账户JWT(二选一)
|
* 2. 验证密码或账户JWT(二选一)
|
||||||
* 3. 适用于需要设备上下文的接口
|
* 3. 适用于需要设备上下文的接口
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {prisma} from "../utils/prisma.js";
|
|
||||||
import errors from "../utils/errors.js";
|
import errors from "../utils/errors.js";
|
||||||
import {verifyToken as verifyAccountJWT} from "../utils/jwt.js";
|
import {verifyToken as verifyAccountJWT} from "../utils/jwt.js";
|
||||||
import {verifyDevicePassword} from "../utils/crypto.js";
|
import {verifyDevicePassword} from "../utils/crypto.js";
|
||||||
|
import { prisma } from "../utils/prisma.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UUID+密码/JWT混合认证中间件
|
* UUID+密码/JWT混合认证中间件
|
||||||
@ -33,7 +32,7 @@ export const uuidAuth = async (req, res, next) => {
|
|||||||
|
|
||||||
// 存储设备信息到locals
|
// 存储设备信息到locals
|
||||||
res.locals.device = device;
|
res.locals.device = device;
|
||||||
res.locals.deviceid = device.id;
|
res.locals.deviceId = device.id;
|
||||||
|
|
||||||
// 3. 验证密码或JWT(二选一)
|
// 3. 验证密码或JWT(二选一)
|
||||||
const password = extractPassword(req);
|
const password = extractPassword(req);
|
||||||
@ -104,7 +103,7 @@ export const extractDeviceInfo = async (req, res, next) => {
|
|||||||
throw errors.createError(404, "设备不存在");
|
throw errors.createError(404, "设备不存在");
|
||||||
}
|
}
|
||||||
res.locals.device = device;
|
res.locals.device = device;
|
||||||
res.locals.deviceid = device.id;
|
res.locals.deviceId = device.id;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ClassworksKV",
|
"name": "ClassworksKV",
|
||||||
"version": "1.3.11",
|
"version": "1.3.15",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ClassworksKV",
|
"name": "ClassworksKV",
|
||||||
"version": "1.3.11",
|
"version": "1.3.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/auto-instrumentations-node": "^0.59.0",
|
"@opentelemetry/auto-instrumentations-node": "^0.59.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.205.0",
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.205.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ClassworksKV",
|
"name": "ClassworksKV",
|
||||||
"version": "1.3.11",
|
"version": "1.3.15",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www",
|
"start": "node ./bin/www",
|
||||||
@ -32,12 +32,13 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"morgan": "~1.10.0",
|
"morgan": "~1.10.0",
|
||||||
"node-device-detector": "^2.2.4",
|
"node-device-detector": "^2.2.4",
|
||||||
"pg": "^8.18.0",
|
|
||||||
"prom-client": "^15.1.3",
|
"prom-client": "^15.1.3",
|
||||||
|
"pg": "^8.18.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
"prisma": "^7.3.0"
|
"prisma": "^7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,109 +2,109 @@
|
|||||||
CREATE SCHEMA IF NOT EXISTS "public";
|
CREATE SCHEMA IF NOT EXISTS "public";
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "account" (
|
CREATE TABLE "Account" (
|
||||||
"id" VARCHAR(191) NOT NULL,
|
"id" VARCHAR(191) NOT NULL,
|
||||||
"provider" VARCHAR(191) NOT NULL,
|
"provider" VARCHAR(191) NOT NULL,
|
||||||
"providerid" VARCHAR(191) NOT NULL,
|
"providerId" VARCHAR(191) NOT NULL,
|
||||||
"email" VARCHAR(191),
|
"email" VARCHAR(191),
|
||||||
"name" VARCHAR(191),
|
"name" VARCHAR(191),
|
||||||
"avatarurl" VARCHAR(191),
|
"avatarUrl" VARCHAR(191),
|
||||||
"providerdata" JSON,
|
"providerData" JSON,
|
||||||
"accesstoken" TEXT,
|
"accessToken" TEXT,
|
||||||
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedat" TIMESTAMPTZ(6) NOT NULL,
|
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
|
||||||
"refreshtoken" TEXT,
|
"refreshToken" TEXT,
|
||||||
"refreshtokenexpiry" TIMESTAMPTZ(6),
|
"refreshTokenExpiry" TIMESTAMPTZ(6),
|
||||||
"tokenversion" INTEGER NOT NULL DEFAULT 1,
|
"tokenVersion" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
|
||||||
CONSTRAINT "idx_18048_primary" PRIMARY KEY ("id")
|
CONSTRAINT "idx_18303_PRIMARY" PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "appinstall" (
|
CREATE TABLE "AppInstall" (
|
||||||
"id" VARCHAR(191) NOT NULL,
|
"id" VARCHAR(191) NOT NULL,
|
||||||
"deviceid" INTEGER NOT NULL,
|
"deviceId" INTEGER NOT NULL,
|
||||||
"appid" VARCHAR(191) NOT NULL,
|
"appId" VARCHAR(191) NOT NULL,
|
||||||
"token" VARCHAR(191) NOT NULL,
|
"token" VARCHAR(191) NOT NULL,
|
||||||
"note" VARCHAR(191),
|
"note" VARCHAR(191),
|
||||||
"installedat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"installedAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedat" TIMESTAMPTZ(6) NOT NULL,
|
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
|
||||||
"devicetype" VARCHAR(191),
|
"deviceType" VARCHAR(191),
|
||||||
"isreadonly" BOOLEAN NOT NULL DEFAULT false,
|
"isReadOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
CONSTRAINT "idx_18055_primary" PRIMARY KEY ("id")
|
CONSTRAINT "idx_18310_PRIMARY" PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "autoauth" (
|
CREATE TABLE "AutoAuth" (
|
||||||
"id" VARCHAR(191) NOT NULL,
|
"id" VARCHAR(191) NOT NULL,
|
||||||
"deviceid" INTEGER NOT NULL,
|
"deviceId" INTEGER NOT NULL,
|
||||||
"password" VARCHAR(191),
|
"password" VARCHAR(191),
|
||||||
"devicetype" VARCHAR(191),
|
"deviceType" VARCHAR(191),
|
||||||
"isreadonly" BOOLEAN NOT NULL DEFAULT false,
|
"isReadOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||||
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedat" TIMESTAMPTZ(6) NOT NULL,
|
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
|
||||||
|
|
||||||
CONSTRAINT "idx_18062_primary" PRIMARY KEY ("id")
|
CONSTRAINT "idx_18317_PRIMARY" PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "device" (
|
CREATE TABLE "Device" (
|
||||||
"id" INTEGER NOT NULL,
|
"id" INTEGER NOT NULL,
|
||||||
"uuid" VARCHAR(191) NOT NULL,
|
"uuid" VARCHAR(191) NOT NULL,
|
||||||
"name" VARCHAR(191),
|
"name" VARCHAR(191),
|
||||||
"accountid" VARCHAR(191),
|
"accountId" VARCHAR(191),
|
||||||
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedat" TIMESTAMPTZ(6) NOT NULL,
|
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
|
||||||
"password" VARCHAR(191),
|
"password" VARCHAR(191),
|
||||||
"passwordhint" VARCHAR(191),
|
"passwordHint" VARCHAR(191),
|
||||||
"namespace" VARCHAR(191),
|
"namespace" VARCHAR(191),
|
||||||
|
|
||||||
CONSTRAINT "idx_18069_primary" PRIMARY KEY ("id")
|
CONSTRAINT "idx_18324_PRIMARY" PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "kvstore" (
|
CREATE TABLE "KVStore" (
|
||||||
"deviceid" INTEGER NOT NULL,
|
"deviceId" INTEGER NOT NULL,
|
||||||
"key" VARCHAR(191) NOT NULL,
|
"key" VARCHAR(191) NOT NULL,
|
||||||
"value" JSON NOT NULL,
|
"value" JSON NOT NULL,
|
||||||
"creatorip" VARCHAR(191) DEFAULT '',
|
"creatorIp" VARCHAR(191) DEFAULT '',
|
||||||
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedat" TIMESTAMPTZ(6) NOT NULL,
|
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
|
||||||
|
|
||||||
CONSTRAINT "idx_18075_primary" PRIMARY KEY ("deviceid","key")
|
CONSTRAINT "idx_18330_PRIMARY" PRIMARY KEY ("deviceId","key")
|
||||||
);
|
);
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "idx_18048_account_provider_providerid_key" ON "account"("provider", "providerid");
|
CREATE UNIQUE INDEX "idx_18303_Account_provider_providerId_key" ON "Account"("provider", "providerId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "idx_18055_appinstall_token_key" ON "appinstall"("token");
|
CREATE UNIQUE INDEX "idx_18310_AppInstall_token_key" ON "AppInstall"("token");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "idx_18055_appinstall_deviceid_fkey" ON "appinstall"("deviceid");
|
CREATE INDEX "idx_18310_AppInstall_deviceId_fkey" ON "AppInstall"("deviceId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "idx_18062_autoauth_deviceid_password_key" ON "autoauth"("deviceid", "password");
|
CREATE UNIQUE INDEX "idx_18317_AutoAuth_deviceId_password_key" ON "AutoAuth"("deviceId", "password");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "idx_18069_device_uuid_key" ON "device"("uuid");
|
CREATE UNIQUE INDEX "idx_18324_Device_uuid_key" ON "Device"("uuid");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "idx_18069_device_namespace_key" ON "device"("namespace");
|
CREATE UNIQUE INDEX "idx_18324_Device_namespace_key" ON "Device"("namespace");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "idx_18069_device_accountid_fkey" ON "device"("accountid");
|
CREATE INDEX "idx_18324_Device_accountId_fkey" ON "Device"("accountId");
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "appinstall" ADD CONSTRAINT "appinstall_deviceid_fkey" FOREIGN KEY ("deviceid") REFERENCES "device"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "AppInstall" ADD CONSTRAINT "AppInstall_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "autoauth" ADD CONSTRAINT "autoauth_deviceid_fkey" FOREIGN KEY ("deviceid") REFERENCES "device"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "AutoAuth" ADD CONSTRAINT "AutoAuth_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "device" ADD CONSTRAINT "device_accountid_fkey" FOREIGN KEY ("accountid") REFERENCES "account"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
ALTER TABLE "Device" ADD CONSTRAINT "Device_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "kvstore" ADD CONSTRAINT "kvstore_deviceid_fkey" FOREIGN KEY ("deviceid") REFERENCES "device"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "KVStore" ADD CONSTRAINT "KVStore_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "account" ALTER COLUMN "createdat" SET DEFAULT timezone('Asia/Shanghai', now()),
|
|
||||||
ALTER COLUMN "updatedat" SET DEFAULT timezone('Asia/Shanghai', now());
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "appinstall" ALTER COLUMN "installedat" SET DEFAULT timezone('Asia/Shanghai', now()),
|
|
||||||
ALTER COLUMN "updatedat" SET DEFAULT timezone('Asia/Shanghai', now());
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "autoauth" ALTER COLUMN "createdat" SET DEFAULT timezone('Asia/Shanghai', now()),
|
|
||||||
ALTER COLUMN "updatedat" SET DEFAULT timezone('Asia/Shanghai', now());
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "device" ALTER COLUMN "createdat" SET DEFAULT timezone('Asia/Shanghai', now()),
|
|
||||||
ALTER COLUMN "updatedat" SET DEFAULT timezone('Asia/Shanghai', now());
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "kvstore" ALTER COLUMN "createdat" SET DEFAULT timezone('Asia/Shanghai', now()),
|
|
||||||
ALTER COLUMN "updatedat" SET DEFAULT timezone('Asia/Shanghai', now());
|
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Account" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "AppInstall" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "AutoAuth" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Device" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "KVStore" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;
|
||||||
4
prisma/migrations/20260201131541_fix_idbug/migration.sql
Normal file
4
prisma/migrations/20260201131541_fix_idbug/migration.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
CREATE SEQUENCE device_id_seq;
|
||||||
|
ALTER TABLE "Device" ALTER COLUMN "id" SET DEFAULT nextval('device_id_seq');
|
||||||
|
ALTER SEQUENCE device_id_seq OWNED BY "Device"."id";
|
||||||
@ -7,79 +7,84 @@ datasource db {
|
|||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
}
|
}
|
||||||
|
|
||||||
model account {
|
model Account {
|
||||||
id String @id(map: "idx_18048_primary") @db.VarChar(191)
|
id String @id(map: "idx_18303_PRIMARY") @db.VarChar(191) @default(cuid())
|
||||||
provider String @db.VarChar(191)
|
provider String @db.VarChar(191)
|
||||||
providerid String @db.VarChar(191)
|
providerId String @db.VarChar(191)
|
||||||
email String? @db.VarChar(191)
|
email String? @db.VarChar(191)
|
||||||
name String? @db.VarChar(191)
|
name String? @db.VarChar(191)
|
||||||
avatarurl String? @db.VarChar(191)
|
avatarUrl String? @db.VarChar(191)
|
||||||
providerdata Json? @db.Json
|
providerData Json? @db.Json
|
||||||
accesstoken String?
|
accessToken String?
|
||||||
createdat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
updatedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @updatedAt @db.Timestamptz(6)
|
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
|
||||||
refreshtoken String?
|
refreshToken String?
|
||||||
refreshtokenexpiry DateTime? @db.Timestamptz(6)
|
refreshTokenExpiry DateTime? @db.Timestamptz(6)
|
||||||
tokenversion Int @default(1)
|
tokenVersion Int @default(1)
|
||||||
device device[]
|
|
||||||
|
|
||||||
@@unique([provider, providerid], map: "idx_18048_account_provider_providerid_key")
|
devices Device[]
|
||||||
|
|
||||||
|
@@unique([provider, providerId], map: "idx_18303_Account_provider_providerId_key")
|
||||||
}
|
}
|
||||||
|
|
||||||
model appinstall {
|
model AppInstall {
|
||||||
id String @id(map: "idx_18055_primary") @db.VarChar(191)
|
id String @id(map: "idx_18310_PRIMARY") @default(cuid()) @db.VarChar(191)
|
||||||
deviceid Int
|
deviceId Int
|
||||||
appid String @db.VarChar(191)
|
appId String @db.VarChar(191)
|
||||||
token String @unique(map: "idx_18055_appinstall_token_key") @db.VarChar(191)
|
token String @unique(map: "idx_18310_AppInstall_token_key") @db.VarChar(191)
|
||||||
note String? @db.VarChar(191)
|
note String? @db.VarChar(191)
|
||||||
installedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @db.Timestamptz(6)
|
installedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
updatedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @updatedAt @db.Timestamptz(6)
|
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
|
||||||
devicetype String? @db.VarChar(191)
|
deviceType String? @db.VarChar(191)
|
||||||
isreadonly Boolean @default(false)
|
isReadOnly Boolean @default(false)
|
||||||
device device @relation(fields: [deviceid], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([deviceid], map: "idx_18055_appinstall_deviceid_fkey")
|
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([deviceId], map: "idx_18310_AppInstall_deviceId_fkey")
|
||||||
}
|
}
|
||||||
|
|
||||||
model autoauth {
|
model AutoAuth {
|
||||||
id String @id(map: "idx_18062_primary") @db.VarChar(191)
|
id String @id(map: "idx_18317_PRIMARY") @default(cuid()) @db.VarChar(191)
|
||||||
deviceid Int
|
deviceId Int
|
||||||
password String? @db.VarChar(191)
|
password String? @db.VarChar(191)
|
||||||
devicetype String? @db.VarChar(191)
|
deviceType String? @db.VarChar(191)
|
||||||
isreadonly Boolean @default(false)
|
isReadOnly Boolean @default(false)
|
||||||
createdat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
updatedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @updatedAt @db.Timestamptz(6)
|
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
|
||||||
device device @relation(fields: [deviceid], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([deviceid, password], map: "idx_18062_autoauth_deviceid_password_key")
|
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([deviceId, password], map: "idx_18317_AutoAuth_deviceId_password_key")
|
||||||
}
|
}
|
||||||
|
|
||||||
model device {
|
model Device {
|
||||||
id Int @id(map: "idx_18069_primary")
|
id Int @id(map: "idx_18324_PRIMARY") @default(autoincrement())
|
||||||
uuid String @unique(map: "idx_18069_device_uuid_key") @db.VarChar(191)
|
uuid String @unique(map: "idx_18324_Device_uuid_key") @db.VarChar(191)
|
||||||
name String? @db.VarChar(191)
|
name String? @db.VarChar(191)
|
||||||
accountid String? @db.VarChar(191)
|
accountId String? @db.VarChar(191)
|
||||||
createdat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
updatedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @updatedAt @db.Timestamptz(6)
|
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
|
||||||
password String? @db.VarChar(191)
|
password String? @db.VarChar(191)
|
||||||
passwordhint String? @db.VarChar(191)
|
passwordHint String? @db.VarChar(191)
|
||||||
namespace String? @unique(map: "idx_18069_device_namespace_key") @db.VarChar(191)
|
namespace String? @unique(map: "idx_18324_Device_namespace_key") @db.VarChar(191)
|
||||||
appinstall appinstall[]
|
|
||||||
autoauth autoauth[]
|
|
||||||
account account? @relation(fields: [accountid], references: [id])
|
|
||||||
kvstore kvstore[]
|
|
||||||
|
|
||||||
@@index([accountid], map: "idx_18069_device_accountid_fkey")
|
// 关联关系
|
||||||
|
account Account? @relation(fields: [accountId], references: [id], onDelete: SetNull)
|
||||||
|
appInstalls AppInstall[]
|
||||||
|
kvStore KVStore[] // 设备相关的KV存储
|
||||||
|
autoAuths AutoAuth[] // 自动授权配置
|
||||||
|
@@index([accountId], map: "idx_18324_Device_accountId_fkey")
|
||||||
}
|
}
|
||||||
|
|
||||||
model kvstore {
|
model KVStore {
|
||||||
deviceid Int
|
deviceId Int
|
||||||
key String @db.VarChar(191)
|
key String @db.VarChar(191)
|
||||||
value Json @db.Json
|
value Json @db.Json
|
||||||
creatorip String? @default("") @db.VarChar(191)
|
creatorIp String? @default("") @db.VarChar(191)
|
||||||
createdat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
updatedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @updatedAt @db.Timestamptz(6)
|
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
|
||||||
device device @relation(fields: [deviceid], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@id([deviceid, key], map: "idx_18075_primary")
|
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([deviceId, key], map: "idx_18330_PRIMARY")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import {Router} from "express";
|
import {Router} from "express";
|
||||||
import {prisma} from "../utils/prisma.js";
|
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import {generateState, getCallbackURL, oauthProviders} from "../config/oauth.js";
|
import {generateState, getCallbackURL, oauthProviders} from "../config/oauth.js";
|
||||||
import {generateTokenPair, refreshAccessToken, revokeAllTokens, revokeRefreshToken} from "../utils/jwt.js";
|
import {generateTokenPair, refreshAccessToken, revokeAllTokens, revokeRefreshToken} from "../utils/jwt.js";
|
||||||
import {jwtAuth} from "../middleware/jwt-auth.js";
|
import {jwtAuth} from "../middleware/jwt-auth.js";
|
||||||
import errors from "../utils/errors.js";
|
import errors from "../utils/errors.js";
|
||||||
|
import { prisma } from "../utils/prisma.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -232,10 +232,10 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
|||||||
|
|
||||||
// 2. 使用访问令牌获取用户信息
|
// 2. 使用访问令牌获取用户信息
|
||||||
let userResponse;
|
let userResponse;
|
||||||
// Casdoor 支持两种方式:Authorization Bearer 或 accesstoken 查询参数
|
// Casdoor 支持两种方式:Authorization Bearer 或 accessToken 查询参数
|
||||||
if (provider === 'stcn') {
|
if (provider === 'stcn') {
|
||||||
const url = new URL(providerConfig.userInfoURL);
|
const url = new URL(providerConfig.userInfoURL);
|
||||||
url.searchParams.set('accesstoken', tokenData.access_token);
|
url.searchParams.set('accessToken', tokenData.access_token);
|
||||||
userResponse = await fetch(url, {headers: {"Accept": "application/json"}});
|
userResponse = await fetch(url, {headers: {"Accept": "application/json"}});
|
||||||
} else {
|
} else {
|
||||||
userResponse = await fetch(providerConfig.userInfoURL, {
|
userResponse = await fetch(providerConfig.userInfoURL, {
|
||||||
@ -256,14 +256,14 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
|||||||
providerId: String(userData.id),
|
providerId: String(userData.id),
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
name: userData.name || userData.login,
|
name: userData.name || userData.login,
|
||||||
avatarurl: userData.avatar_url,
|
avatarUrl: userData.avatar_url,
|
||||||
};
|
};
|
||||||
} else if (provider === "zerocat") {
|
} else if (provider === "zerocat") {
|
||||||
normalizedUser = {
|
normalizedUser = {
|
||||||
providerId: userData.openid,
|
providerId: userData.openid,
|
||||||
email: userData.email_verified ? userData.email : null,
|
email: userData.email_verified ? userData.email : null,
|
||||||
name: userData.nickname || userData.username,
|
name: userData.nickname || userData.username,
|
||||||
avatarurl: userData.avatar,
|
avatarUrl: userData.avatar,
|
||||||
};
|
};
|
||||||
} else if (provider === "hly") {
|
} else if (provider === "hly") {
|
||||||
// 厚浪云(Logto)标准OIDC用户信息
|
// 厚浪云(Logto)标准OIDC用户信息
|
||||||
@ -271,7 +271,7 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
|||||||
providerId: userData.sub,
|
providerId: userData.sub,
|
||||||
email: userData.email_verified ? userData.email : null,
|
email: userData.email_verified ? userData.email : null,
|
||||||
name: userData.name || userData.preferred_username || userData.nickname,
|
name: userData.name || userData.preferred_username || userData.nickname,
|
||||||
avatarurl: userData.picture,
|
avatarUrl: userData.picture,
|
||||||
};
|
};
|
||||||
} else if (provider === "stcn") {
|
} else if (provider === "stcn") {
|
||||||
// STCN(Casdoor)标准OIDC用户信息
|
// STCN(Casdoor)标准OIDC用户信息
|
||||||
@ -279,7 +279,7 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
|||||||
providerId: userData.sub,
|
providerId: userData.sub,
|
||||||
email: userData.email_verified ? userData.email : userData.email || null,
|
email: userData.email_verified ? userData.email : userData.email || null,
|
||||||
name: userData.name || userData.preferred_username || userData.nickname,
|
name: userData.name || userData.preferred_username || userData.nickname,
|
||||||
avatarurl: userData.picture,
|
avatarUrl: userData.picture,
|
||||||
};
|
};
|
||||||
} else if (provider === "dlass") {
|
} else if (provider === "dlass") {
|
||||||
// Dlass(Casdoor)标准OIDC用户信息
|
// Dlass(Casdoor)标准OIDC用户信息
|
||||||
@ -287,7 +287,7 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
|||||||
providerId: userData.sub,
|
providerId: userData.sub,
|
||||||
email: userData.email_verified ? userData.email : userData.email || null,
|
email: userData.email_verified ? userData.email : userData.email || null,
|
||||||
name: userData.name || userData.preferred_username || userData.nickname,
|
name: userData.name || userData.preferred_username || userData.nickname,
|
||||||
avatarurl: userData.picture,
|
avatarUrl: userData.picture,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,25 +316,25 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
|||||||
data: {
|
data: {
|
||||||
email: normalizedUser.email || account.email,
|
email: normalizedUser.email || account.email,
|
||||||
name: normalizedUser.name || account.name,
|
name: normalizedUser.name || account.name,
|
||||||
avatarurl: normalizedUser.avatarurl || account.avatarurl,
|
avatarUrl: normalizedUser.avatarUrl || account.avatarUrl,
|
||||||
providerData: userData,
|
providerData: userData,
|
||||||
//refreshtoken: tokenData.refresh_token || account.refreshtoken,
|
//refreshToken: tokenData.refresh_token || account.refreshToken,
|
||||||
updatedat: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 创建新账户
|
// 创建新账户
|
||||||
const accesstoken = generateAccessToken();
|
const accessToken = generateAccessToken();
|
||||||
account = await prisma.account.create({
|
account = await prisma.account.create({
|
||||||
data: {
|
data: {
|
||||||
provider,
|
provider,
|
||||||
providerId: normalizedUser.providerId,
|
providerId: normalizedUser.providerId,
|
||||||
email: normalizedUser.email,
|
email: normalizedUser.email,
|
||||||
name: normalizedUser.name,
|
name: normalizedUser.name,
|
||||||
avatarurl: normalizedUser.avatarurl,
|
avatarUrl: normalizedUser.avatarUrl,
|
||||||
providerData: userData,
|
providerData: userData,
|
||||||
accesstoken,
|
accessToken,
|
||||||
//refreshtoken: tokenData.refresh_token,
|
//refreshToken: tokenData.refresh_token,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -345,9 +345,9 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
|||||||
// 6. 重定向到前端根路径,携带JWT token
|
// 6. 重定向到前端根路径,携带JWT token
|
||||||
const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
|
const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
|
||||||
const callbackUrl = new URL(frontendBaseUrl);
|
const callbackUrl = new URL(frontendBaseUrl);
|
||||||
callbackUrl.searchParams.append("access_token", tokens.accesstoken);
|
callbackUrl.searchParams.append("access_token", tokens.accessToken);
|
||||||
callbackUrl.searchParams.append("refresh_token", tokens.refreshtoken);
|
callbackUrl.searchParams.append("refresh_token", tokens.refreshToken);
|
||||||
callbackUrl.searchParams.append("expires_in", tokens.accesstokenExpiresIn);
|
callbackUrl.searchParams.append("expires_in", tokens.accessTokenExpiresIn);
|
||||||
callbackUrl.searchParams.append("provider", provider);
|
callbackUrl.searchParams.append("provider", provider);
|
||||||
// 附带展示信息,便于前端显示品牌与名称
|
// 附带展示信息,便于前端显示品牌与名称
|
||||||
const pconf = oauthProviders[provider] || {};
|
const pconf = oauthProviders[provider] || {};
|
||||||
@ -392,7 +392,7 @@ router.get("/profile", jwtAuth, async (req, res, next) => {
|
|||||||
id: true,
|
id: true,
|
||||||
uuid: true,
|
uuid: true,
|
||||||
name: true,
|
name: true,
|
||||||
createdat: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -421,9 +421,9 @@ router.get("/profile", jwtAuth, async (req, res, next) => {
|
|||||||
providerInfo,
|
providerInfo,
|
||||||
email: account.email,
|
email: account.email,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
avatarurl: account.avatarurl,
|
avatarUrl: account.avatarUrl,
|
||||||
devices: account.devices,
|
devices: account.devices,
|
||||||
createdat: account.createdat,
|
createdAt: account.createdAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -487,7 +487,7 @@ router.post("/devices/bind", jwtAuth, async (req, res, next) => {
|
|||||||
success: true,
|
success: true,
|
||||||
message: "设备绑定成功",
|
message: "设备绑定成功",
|
||||||
data: {
|
data: {
|
||||||
deviceid: updatedDevice.id,
|
deviceId: updatedDevice.id,
|
||||||
uuid: updatedDevice.uuid,
|
uuid: updatedDevice.uuid,
|
||||||
name: updatedDevice.name,
|
name: updatedDevice.name,
|
||||||
},
|
},
|
||||||
@ -592,8 +592,8 @@ router.get("/devices", jwtAuth, async (req, res, next) => {
|
|||||||
uuid: true,
|
uuid: true,
|
||||||
name: true,
|
name: true,
|
||||||
namespace: true,
|
namespace: true,
|
||||||
createdat: true,
|
createdAt: true,
|
||||||
updatedat: true,
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -627,8 +627,8 @@ router.get("/device/:uuid/account", async (req, res, next) => {
|
|||||||
id: true,
|
id: true,
|
||||||
provider: true,
|
provider: true,
|
||||||
name: true,
|
name: true,
|
||||||
avatarurl: true,
|
avatarUrl: true,
|
||||||
createdat: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -654,8 +654,8 @@ router.get("/device/:uuid/account", async (req, res, next) => {
|
|||||||
id: device.account.id,
|
id: device.account.id,
|
||||||
provider: device.account.provider,
|
provider: device.account.provider,
|
||||||
name: device.account.name,
|
name: device.account.name,
|
||||||
avatarurl: device.account.avatarurl,
|
avatarUrl: device.account.avatarUrl,
|
||||||
bindTime: device.updatedat, // 绑定时间
|
bindTime: device.updatedAt, // 绑定时间
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -690,8 +690,8 @@ router.post("/refresh", async (req, res, next) => {
|
|||||||
success: true,
|
success: true,
|
||||||
message: "令牌刷新成功",
|
message: "令牌刷新成功",
|
||||||
data: {
|
data: {
|
||||||
access_token: result.accesstoken,
|
access_token: result.accessToken,
|
||||||
expires_in: result.accesstokenExpiresIn,
|
expires_in: result.accessTokenExpiresIn,
|
||||||
account: result.account,
|
account: result.account,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -780,14 +780,14 @@ router.get("/token-info", jwtAuth, async (req, res, next) => {
|
|||||||
data: {
|
data: {
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
tokenType: decoded.type || 'legacy',
|
tokenType: decoded.type || 'legacy',
|
||||||
tokenversion: decoded.tokenversion || account.tokenversion,
|
tokenVersion: decoded.tokenVersion || account.tokenVersion,
|
||||||
issuedAt: new Date(decoded.iat * 1000),
|
issuedAt: new Date(decoded.iat * 1000),
|
||||||
expiresAt: new Date(decoded.exp * 1000),
|
expiresAt: new Date(decoded.exp * 1000),
|
||||||
expiresIn: expiresIn,
|
expiresIn: expiresIn,
|
||||||
isExpired: expiresIn <= 0,
|
isExpired: expiresIn <= 0,
|
||||||
isLegacyToken: res.locals.isLegacyToken || false,
|
isLegacyToken: res.locals.isLegacyToken || false,
|
||||||
hasRefreshToken: !!account.refreshtoken,
|
hasRefreshToken: !!account.refreshToken,
|
||||||
refreshtokenExpiry: account.refreshtokenExpiry,
|
refreshTokenExpiry: account.refreshTokenExpiry,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import {Router} from "express";
|
import {Router} from "express";
|
||||||
import {uuidAuth} from "../middleware/uuidAuth.js";
|
import {uuidAuth} from "../middleware/uuidAuth.js";
|
||||||
import {prisma} from "../utils/prisma.js";
|
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import errors from "../utils/errors.js";
|
import errors from "../utils/errors.js";
|
||||||
import {verifyDevicePassword} from "../utils/crypto.js";
|
import {verifyDevicePassword} from "../utils/crypto.js";
|
||||||
|
import { prisma } from "../utils/prisma.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -25,15 +25,15 @@ router.get(
|
|||||||
return next(errors.createError(404, "设备不存在"));
|
return next(errors.createError(404, "设备不存在"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const installations = await prisma.appinstall.findMany({
|
const installations = await prisma.appInstall.findMany({
|
||||||
where: {deviceid: device.id},
|
where: {deviceId: device.id},
|
||||||
});
|
});
|
||||||
|
|
||||||
const apps = installations.map(install => ({
|
const apps = installations.map(install => ({
|
||||||
appId: install.appId,
|
appId: install.appId,
|
||||||
token: install.token,
|
token: install.token,
|
||||||
note: install.note,
|
note: install.note,
|
||||||
installedAt: install.createdat,
|
installedAt: install.createdAt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
@ -60,9 +60,9 @@ router.post(
|
|||||||
const token = crypto.randomBytes(32).toString("hex");
|
const token = crypto.randomBytes(32).toString("hex");
|
||||||
|
|
||||||
// 创建安装记录
|
// 创建安装记录
|
||||||
const installation = await prisma.appinstall.create({
|
const installation = await prisma.appInstall.create({
|
||||||
data: {
|
data: {
|
||||||
deviceid: device.id,
|
deviceId: device.id,
|
||||||
appId: appId,
|
appId: appId,
|
||||||
token,
|
token,
|
||||||
note: note || null,
|
note: note || null,
|
||||||
@ -75,7 +75,7 @@ router.post(
|
|||||||
token: installation.token,
|
token: installation.token,
|
||||||
note: installation.note,
|
note: installation.note,
|
||||||
name: installation.note, // 备注同时作为名称返回
|
name: installation.note, // 备注同时作为名称返回
|
||||||
installedAt: installation.createdat,
|
installedAt: installation.createdAt,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -91,7 +91,7 @@ router.delete(
|
|||||||
const device = res.locals.device;
|
const device = res.locals.device;
|
||||||
const {installId} = req.params;
|
const {installId} = req.params;
|
||||||
|
|
||||||
const installation = await prisma.appinstall.findUnique({
|
const installation = await prisma.appInstall.findUnique({
|
||||||
where: {id: installId},
|
where: {id: installId},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -100,11 +100,11 @@ router.delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保安装记录属于当前设备
|
// 确保安装记录属于当前设备
|
||||||
if (installation.deviceid !== device.id) {
|
if (installation.deviceId !== device.id) {
|
||||||
return next(errors.createError(403, "无权操作此安装记录"));
|
return next(errors.createError(403, "无权操作此安装记录"));
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.appinstall.delete({
|
await prisma.appInstall.delete({
|
||||||
where: {id: installation.id},
|
where: {id: installation.id},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -135,8 +135,8 @@ router.get(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取该设备的所有应用安装记录(即token)
|
// 获取该设备的所有应用安装记录(即token)
|
||||||
const installations = await prisma.appinstall.findMany({
|
const installations = await prisma.appInstall.findMany({
|
||||||
where: {deviceid: device.id},
|
where: {deviceId: device.id},
|
||||||
orderBy: {installedAt: 'desc'},
|
orderBy: {installedAt: 'desc'},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -235,22 +235,22 @@ router.post(
|
|||||||
// 根据自动授权配置创建 AppInstall
|
// 根据自动授权配置创建 AppInstall
|
||||||
const token = crypto.randomBytes(32).toString("hex");
|
const token = crypto.randomBytes(32).toString("hex");
|
||||||
|
|
||||||
const installation = await prisma.appinstall.create({
|
const installation = await prisma.appInstall.create({
|
||||||
data: {
|
data: {
|
||||||
deviceid: device.id,
|
deviceId: device.id,
|
||||||
appId: appId,
|
appId: appId,
|
||||||
token,
|
token,
|
||||||
note: null,
|
note: null,
|
||||||
isreadonly: matchedAutoAuth.isreadonly,
|
isReadOnly: matchedAutoAuth.isReadOnly,
|
||||||
devicetype: matchedAutoAuth.devicetype,
|
deviceType: matchedAutoAuth.deviceType,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
token: installation.token,
|
token: installation.token,
|
||||||
devicetype: installation.devicetype,
|
deviceType: installation.deviceType,
|
||||||
isreadonly: installation.isreadonly,
|
isReadOnly: installation.isReadOnly,
|
||||||
installedAt: installation.installedAt,
|
installedAt: installation.installedAt,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -272,7 +272,7 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查找 token 对应的应用安装记录
|
// 查找 token 对应的应用安装记录
|
||||||
const appInstall = await prisma.appinstall.findUnique({
|
const appInstall = await prisma.appInstall.findUnique({
|
||||||
where: {token},
|
where: {token},
|
||||||
include: {
|
include: {
|
||||||
device: true,
|
device: true,
|
||||||
@ -284,15 +284,15 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证 token 类型是否为 student
|
// 验证 token 类型是否为 student
|
||||||
if (!['student', 'parent'].includes(appInstall.devicetype)) {
|
if (!['student', 'parent'].includes(appInstall.deviceType)) {
|
||||||
return next(errors.createError(403, "只有学生和家长类型的 token 可以设置名称"));
|
return next(errors.createError(403, "只有学生和家长类型的 token 可以设置名称"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取设备的 classworks-list-main 键值
|
// 读取设备的 classworks-list-main 键值
|
||||||
const kvRecord = await prisma.kvstore.findUnique({
|
const kvRecord = await prisma.kVStore.findUnique({
|
||||||
where: {
|
where: {
|
||||||
deviceid_key: {
|
deviceId_key: {
|
||||||
deviceid: appInstall.deviceid,
|
deviceId: appInstall.deviceId,
|
||||||
key: 'classworks-list-main',
|
key: 'classworks-list-main',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -321,17 +321,17 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新 AppInstall 的 note 字段
|
// 更新 AppInstall 的 note 字段
|
||||||
const updatedInstall = await prisma.appinstall.update({
|
const updatedInstall = await prisma.appInstall.update({
|
||||||
where: {id: appInstall.id},
|
where: {id: appInstall.id},
|
||||||
data: {note: appInstall.devicetype === 'parent' ? `${name} 家长` : name},
|
data: {note: appInstall.deviceType === 'parent' ? `${name} 家长` : name},
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
token: updatedInstall.token,
|
token: updatedInstall.token,
|
||||||
name: updatedInstall.note,
|
name: updatedInstall.note,
|
||||||
devicetype: updatedInstall.devicetype,
|
deviceType: updatedInstall.deviceType,
|
||||||
updatedat: updatedInstall.updatedat,
|
updatedAt: updatedInstall.updatedAt,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -352,7 +352,7 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查找 token 对应的应用安装记录
|
// 查找 token 对应的应用安装记录
|
||||||
const appInstall = await prisma.appinstall.findUnique({
|
const appInstall = await prisma.appInstall.findUnique({
|
||||||
where: {token},
|
where: {token},
|
||||||
include: {
|
include: {
|
||||||
device: true,
|
device: true,
|
||||||
@ -364,12 +364,12 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证 token 类型是否为 teacher
|
// 验证 token 类型是否为 teacher
|
||||||
if (appInstall.devicetype !== 'teacher') {
|
if (appInstall.deviceType !== 'teacher') {
|
||||||
return next(errors.createError(403, "只有教师类型的 token 可以使用此接口"));
|
return next(errors.createError(403, "只有教师类型的 token 可以使用此接口"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 AppInstall 的 note 字段为教师名称
|
// 更新 AppInstall 的 note 字段为教师名称
|
||||||
const updatedInstall = await prisma.appinstall.update({
|
const updatedInstall = await prisma.appInstall.update({
|
||||||
where: {id: appInstall.id},
|
where: {id: appInstall.id},
|
||||||
data: {note: name},
|
data: {note: name},
|
||||||
});
|
});
|
||||||
@ -378,8 +378,8 @@ router.post(
|
|||||||
success: true,
|
success: true,
|
||||||
token: updatedInstall.token,
|
token: updatedInstall.token,
|
||||||
name: updatedInstall.note,
|
name: updatedInstall.note,
|
||||||
devicetype: updatedInstall.devicetype,
|
deviceType: updatedInstall.deviceType,
|
||||||
updatedat: updatedInstall.updatedat,
|
updatedAt: updatedInstall.updatedAt,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -396,7 +396,7 @@ router.put(
|
|||||||
const {note} = req.body;
|
const {note} = req.body;
|
||||||
|
|
||||||
// 查找 token 对应的应用安装记录
|
// 查找 token 对应的应用安装记录
|
||||||
const appInstall = await prisma.appinstall.findUnique({
|
const appInstall = await prisma.appInstall.findUnique({
|
||||||
where: {token},
|
where: {token},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -405,7 +405,7 @@ router.put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新 AppInstall 的 note 字段
|
// 更新 AppInstall 的 note 字段
|
||||||
const updatedInstall = await prisma.appinstall.update({
|
const updatedInstall = await prisma.appInstall.update({
|
||||||
where: {id: appInstall.id},
|
where: {id: appInstall.id},
|
||||||
data: {note: note || null},
|
data: {note: note || null},
|
||||||
});
|
});
|
||||||
@ -414,7 +414,7 @@ router.put(
|
|||||||
success: true,
|
success: true,
|
||||||
token: updatedInstall.token,
|
token: updatedInstall.token,
|
||||||
note: updatedInstall.note,
|
note: updatedInstall.note,
|
||||||
updatedat: updatedInstall.updatedat,
|
updatedAt: updatedInstall.updatedAt,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import {Router} from "express";
|
import {Router} from "express";
|
||||||
import {jwtAuth} from "../middleware/jwt-auth.js";
|
import {jwtAuth} from "../middleware/jwt-auth.js";
|
||||||
import {prisma} from "../utils/prisma.js";
|
|
||||||
import errors from "../utils/errors.js";
|
import errors from "../utils/errors.js";
|
||||||
|
import { prisma } from "../utils/prisma.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -31,8 +31,8 @@ router.get(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const autoAuths = await prisma.autoAuth.findMany({
|
const autoAuths = await prisma.autoAuth.findMany({
|
||||||
where: {deviceid: device.id},
|
where: {deviceId: device.id},
|
||||||
orderBy: {createdat: 'desc'},
|
orderBy: {createdAt: 'desc'},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 返回配置,智能处理密码显示
|
// 返回配置,智能处理密码显示
|
||||||
@ -44,10 +44,10 @@ router.get(
|
|||||||
id: auth.id,
|
id: auth.id,
|
||||||
password: isHashedPassword ? null : auth.password, // 哈希密码不返回
|
password: isHashedPassword ? null : auth.password, // 哈希密码不返回
|
||||||
isLegacyHash: isHashedPassword, // 标记是否为旧的哈希密码
|
isLegacyHash: isHashedPassword, // 标记是否为旧的哈希密码
|
||||||
devicetype: auth.devicetype,
|
deviceType: auth.deviceType,
|
||||||
isreadonly: auth.isreadonly,
|
isReadOnly: auth.isReadOnly,
|
||||||
createdat: auth.createdat,
|
createdAt: auth.createdAt,
|
||||||
updatedat: auth.updatedat,
|
updatedAt: auth.updatedAt,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ router.get(
|
|||||||
/**
|
/**
|
||||||
* POST /auto-auth/devices/:uuid/auth-configs
|
* POST /auto-auth/devices/:uuid/auth-configs
|
||||||
* 创建新的自动授权配置 (需要 JWT 认证,且设备必须绑定到该账户)
|
* 创建新的自动授权配置 (需要 JWT 认证,且设备必须绑定到该账户)
|
||||||
* Body: { password?: string, devicetype?: string, isreadonly?: boolean }
|
* Body: { password?: string, deviceType?: string, isReadOnly?: boolean }
|
||||||
*/
|
*/
|
||||||
router.post(
|
router.post(
|
||||||
"/devices/:uuid/auth-configs",
|
"/devices/:uuid/auth-configs",
|
||||||
@ -69,7 +69,7 @@ router.post(
|
|||||||
errors.catchAsync(async (req, res, next) => {
|
errors.catchAsync(async (req, res, next) => {
|
||||||
const {uuid} = req.params;
|
const {uuid} = req.params;
|
||||||
const account = res.locals.account;
|
const account = res.locals.account;
|
||||||
const {password, devicetype, isreadonly} = req.body;
|
const {password, deviceType, isReadOnly} = req.body;
|
||||||
|
|
||||||
// 查找设备并验证是否属于当前账户
|
// 查找设备并验证是否属于当前账户
|
||||||
const device = await prisma.device.findUnique({
|
const device = await prisma.device.findUnique({
|
||||||
@ -85,9 +85,9 @@ router.post(
|
|||||||
return next(errors.createError(403, "该设备未绑定到您的账户"));
|
return next(errors.createError(403, "该设备未绑定到您的账户"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证 devicetype 如果提供的话
|
// 验证 deviceType 如果提供的话
|
||||||
const validDeviceTypes = ['teacher', 'student', 'classroom', 'parent'];
|
const validDeviceTypes = ['teacher', 'student', 'classroom', 'parent'];
|
||||||
if (devicetype && !validDeviceTypes.includes(devicetype)) {
|
if (deviceType && !validDeviceTypes.includes(deviceType)) {
|
||||||
return next(errors.createError(400, `设备类型必须是以下之一: ${validDeviceTypes.join(', ')}`));
|
return next(errors.createError(400, `设备类型必须是以下之一: ${validDeviceTypes.join(', ')}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ router.post(
|
|||||||
|
|
||||||
// 查询该设备的所有自动授权配置,本地检查是否存在相同密码
|
// 查询该设备的所有自动授权配置,本地检查是否存在相同密码
|
||||||
const allAuths = await prisma.autoAuth.findMany({
|
const allAuths = await prisma.autoAuth.findMany({
|
||||||
where: {deviceid: device.id},
|
where: {deviceId: device.id},
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingAuth = allAuths.find(auth => auth.password === plainPassword);
|
const existingAuth = allAuths.find(auth => auth.password === plainPassword);
|
||||||
@ -108,10 +108,10 @@ router.post(
|
|||||||
// 创建新的自动授权配置(密码明文存储)
|
// 创建新的自动授权配置(密码明文存储)
|
||||||
const autoAuth = await prisma.autoAuth.create({
|
const autoAuth = await prisma.autoAuth.create({
|
||||||
data: {
|
data: {
|
||||||
deviceid: device.id,
|
deviceId: device.id,
|
||||||
password: plainPassword,
|
password: plainPassword,
|
||||||
devicetype: devicetype || null,
|
deviceType: deviceType || null,
|
||||||
isreadonly: isreadonly || false,
|
isReadOnly: isReadOnly || false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -120,9 +120,9 @@ router.post(
|
|||||||
config: {
|
config: {
|
||||||
id: autoAuth.id,
|
id: autoAuth.id,
|
||||||
password: autoAuth.password, // 返回明文密码
|
password: autoAuth.password, // 返回明文密码
|
||||||
devicetype: autoAuth.devicetype,
|
deviceType: autoAuth.deviceType,
|
||||||
isreadonly: autoAuth.isreadonly,
|
isReadOnly: autoAuth.isReadOnly,
|
||||||
createdat: autoAuth.createdat,
|
createdAt: autoAuth.createdAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -130,7 +130,7 @@ router.post(
|
|||||||
/**
|
/**
|
||||||
* PUT /auto-auth/devices/:uuid/auth-configs/:configId
|
* PUT /auto-auth/devices/:uuid/auth-configs/:configId
|
||||||
* 更新自动授权配置 (需要 JWT 认证,且设备必须绑定到该账户)
|
* 更新自动授权配置 (需要 JWT 认证,且设备必须绑定到该账户)
|
||||||
* Body: { password?: string, devicetype?: string, isreadonly?: boolean }
|
* Body: { password?: string, deviceType?: string, isReadOnly?: boolean }
|
||||||
*/
|
*/
|
||||||
router.put(
|
router.put(
|
||||||
"/devices/:uuid/auth-configs/:configId",
|
"/devices/:uuid/auth-configs/:configId",
|
||||||
@ -138,7 +138,7 @@ router.put(
|
|||||||
errors.catchAsync(async (req, res, next) => {
|
errors.catchAsync(async (req, res, next) => {
|
||||||
const {uuid, configId} = req.params;
|
const {uuid, configId} = req.params;
|
||||||
const account = res.locals.account;
|
const account = res.locals.account;
|
||||||
const {password, devicetype, isreadonly} = req.body;
|
const {password, deviceType, isReadOnly} = req.body;
|
||||||
|
|
||||||
// 查找设备并验证是否属于当前账户
|
// 查找设备并验证是否属于当前账户
|
||||||
const device = await prisma.device.findUnique({
|
const device = await prisma.device.findUnique({
|
||||||
@ -164,13 +164,13 @@ router.put(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保配置属于当前设备
|
// 确保配置属于当前设备
|
||||||
if (autoAuth.deviceid !== device.id) {
|
if (autoAuth.deviceId !== device.id) {
|
||||||
return next(errors.createError(403, "无权操作此配置"));
|
return next(errors.createError(403, "无权操作此配置"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证 devicetype
|
// 验证 deviceType
|
||||||
const validDeviceTypes = ['teacher', 'student', 'classroom', 'parent'];
|
const validDeviceTypes = ['teacher', 'student', 'classroom', 'parent'];
|
||||||
if (devicetype && !validDeviceTypes.includes(devicetype)) {
|
if (deviceType && !validDeviceTypes.includes(deviceType)) {
|
||||||
return next(errors.createError(400, `设备类型必须是以下之一: ${validDeviceTypes.join(', ')}`));
|
return next(errors.createError(400, `设备类型必须是以下之一: ${validDeviceTypes.join(', ')}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +183,7 @@ router.put(
|
|||||||
|
|
||||||
// 查询该设备的所有配置,本地检查新密码是否与其他配置冲突
|
// 查询该设备的所有配置,本地检查新密码是否与其他配置冲突
|
||||||
const allAuths = await prisma.autoAuth.findMany({
|
const allAuths = await prisma.autoAuth.findMany({
|
||||||
where: {deviceid: device.id},
|
where: {deviceId: device.id},
|
||||||
});
|
});
|
||||||
|
|
||||||
const conflictAuth = allAuths.find(auth =>
|
const conflictAuth = allAuths.find(auth =>
|
||||||
@ -197,12 +197,12 @@ router.put(
|
|||||||
updateData.password = plainPassword;
|
updateData.password = plainPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (devicetype !== undefined) {
|
if (deviceType !== undefined) {
|
||||||
updateData.devicetype = devicetype || null;
|
updateData.deviceType = deviceType || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isreadonly !== undefined) {
|
if (isReadOnly !== undefined) {
|
||||||
updateData.isreadonly = isreadonly;
|
updateData.isReadOnly = isReadOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新配置
|
// 更新配置
|
||||||
@ -216,9 +216,9 @@ router.put(
|
|||||||
config: {
|
config: {
|
||||||
id: updatedAuth.id,
|
id: updatedAuth.id,
|
||||||
password: updatedAuth.password, // 返回明文密码
|
password: updatedAuth.password, // 返回明文密码
|
||||||
devicetype: updatedAuth.devicetype,
|
deviceType: updatedAuth.deviceType,
|
||||||
isreadonly: updatedAuth.isreadonly,
|
isReadOnly: updatedAuth.isReadOnly,
|
||||||
updatedat: updatedAuth.updatedat,
|
updatedAt: updatedAuth.updatedAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -259,7 +259,7 @@ router.delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保配置属于当前设备
|
// 确保配置属于当前设备
|
||||||
if (autoAuth.deviceid !== device.id) {
|
if (autoAuth.deviceId !== device.id) {
|
||||||
return next(errors.createError(403, "无权操作此配置"));
|
return next(errors.createError(403, "无权操作此配置"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,7 +334,7 @@ router.put(
|
|||||||
uuid: updatedDevice.uuid,
|
uuid: updatedDevice.uuid,
|
||||||
name: updatedDevice.name,
|
name: updatedDevice.name,
|
||||||
namespace: updatedDevice.namespace,
|
namespace: updatedDevice.namespace,
|
||||||
updatedat: updatedDevice.updatedat,
|
updatedAt: updatedDevice.updatedAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import {Router} from "express";
|
import {Router} from "express";
|
||||||
import deviceCodeStore from "../utils/deviceCodeStore.js";
|
import deviceCodeStore from "../utils/deviceCodeStore.js";
|
||||||
import errors from "../utils/errors.js";
|
import errors from "../utils/errors.js";
|
||||||
import {prisma} from "../utils/prisma.js";
|
import { prisma } from "../utils/prisma.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证token是否有效(检查数据库)
|
// 验证token是否有效(检查数据库)
|
||||||
const appInstall = await prisma.appinstall.findUnique({
|
const appInstall = await prisma.appInstall.findUnique({
|
||||||
where: {token},
|
where: {token},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -193,7 +193,7 @@ router.get(
|
|||||||
exists: true,
|
exists: true,
|
||||||
has_token: status.hasToken,
|
has_token: status.hasToken,
|
||||||
expires_in: Math.floor((status.expiresAt - Date.now()) / 1000),
|
expires_in: Math.floor((status.expiresAt - Date.now()) / 1000),
|
||||||
created_at: status.createdat,
|
created_at: status.createdAt,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
import {Router} from "express";
|
import {Router} from "express";
|
||||||
import {extractDeviceInfo} from "../middleware/uuidAuth.js";
|
import {extractDeviceInfo} from "../middleware/uuidAuth.js";
|
||||||
import {prisma} from "../utils/prisma.js";
|
|
||||||
import errors from "../utils/errors.js";
|
import errors from "../utils/errors.js";
|
||||||
import {getOnlineDevices} from "../utils/socket.js";
|
import {getOnlineDevices} from "../utils/socket.js";
|
||||||
import {registeredDevicesTotal} from "../utils/metrics.js";
|
import {registeredDevicesTotal} from "../utils/metrics.js";
|
||||||
|
import { prisma } from "../utils/prisma.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为新设备创建默认的自动登录配置
|
* 为新设备创建默认的自动登录配置
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
*/
|
*/
|
||||||
async function createDefaultAutoAuth(deviceid) {
|
async function createDefaultAutoAuth(deviceId) {
|
||||||
try {
|
try {
|
||||||
// 创建默认的自动授权配置:不需要密码、类型是classroom(一体机)
|
// 创建默认的自动授权配置:不需要密码、类型是classroom(一体机)
|
||||||
await prisma.autoAuth.create({
|
await prisma.autoAuth.create({
|
||||||
data: {
|
data: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
password: null, // 无密码
|
password: null, // 无密码
|
||||||
devicetype: "classroom", // 一体机类型
|
deviceType: "classroom", // 一体机类型
|
||||||
isreadonly: false, // 非只读
|
isReadOnly: false, // 非只读
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -90,7 +90,7 @@ router.post(
|
|||||||
uuid: device.uuid,
|
uuid: device.uuid,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
namespace: device.namespace,
|
namespace: device.namespace,
|
||||||
createdat: device.createdat,
|
createdAt: device.createdAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -117,7 +117,7 @@ router.get(
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
email: true,
|
email: true,
|
||||||
avatarurl: true,
|
avatarUrl: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -132,13 +132,13 @@ router.get(
|
|||||||
uuid: device.uuid,
|
uuid: device.uuid,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
hasPassword: !!device.password,
|
hasPassword: !!device.password,
|
||||||
passwordhint: device.passwordhint,
|
passwordHint: device.passwordHint,
|
||||||
createdat: device.createdat,
|
createdAt: device.createdAt,
|
||||||
account: device.account ? {
|
account: device.account ? {
|
||||||
id: device.account.id,
|
id: device.account.id,
|
||||||
name: device.account.name,
|
name: device.account.name,
|
||||||
email: device.account.email,
|
email: device.account.email,
|
||||||
avatarurl: device.account.avatarurl,
|
avatarUrl: device.account.avatarUrl,
|
||||||
} : null,
|
} : null,
|
||||||
isBoundToAccount: !!device.account,
|
isBoundToAccount: !!device.account,
|
||||||
namespace: device.namespace,
|
namespace: device.namespace,
|
||||||
@ -172,7 +172,7 @@ router.put(
|
|||||||
uuid: updatedDevice.uuid,
|
uuid: updatedDevice.uuid,
|
||||||
name: updatedDevice.name,
|
name: updatedDevice.name,
|
||||||
hasPassword: !!updatedDevice.password,
|
hasPassword: !!updatedDevice.password,
|
||||||
passwordhint: updatedDevice.passwordhint,
|
passwordHint: updatedDevice.passwordHint,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@ -28,11 +28,11 @@ router.get(
|
|||||||
"/_info",
|
"/_info",
|
||||||
tokenReadLimiter,
|
tokenReadLimiter,
|
||||||
errors.catchAsync(async (req, res, next) => {
|
errors.catchAsync(async (req, res, next) => {
|
||||||
const deviceid = res.locals.deviceid;
|
const deviceId = res.locals.deviceId;
|
||||||
|
|
||||||
// 获取设备信息,包含关联的账号
|
// 获取设备信息,包含关联的账号
|
||||||
const device = await prisma.device.findUnique({
|
const device = await prisma.device.findUnique({
|
||||||
where: { id: deviceid },
|
where: { id: deviceId },
|
||||||
include: {
|
include: {
|
||||||
account: true,
|
account: true,
|
||||||
},
|
},
|
||||||
@ -47,8 +47,8 @@ router.get(
|
|||||||
device: {
|
device: {
|
||||||
id: device.id,
|
id: device.id,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
createdat: device.createdat,
|
createdAt: device.createdAt,
|
||||||
updatedat: device.updatedat,
|
updatedAt: device.updatedAt,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ router.get(
|
|||||||
response.account = {
|
response.account = {
|
||||||
id: device.account.id,
|
id: device.account.id,
|
||||||
name: device.account.name,
|
name: device.account.name,
|
||||||
avatarurl: device.account.avatarurl,
|
avatarUrl: device.account.avatarUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,10 +82,10 @@ router.get(
|
|||||||
tokenReadLimiter,
|
tokenReadLimiter,
|
||||||
errors.catchAsync(async (req, res, next) => {
|
errors.catchAsync(async (req, res, next) => {
|
||||||
const token = res.locals.token;
|
const token = res.locals.token;
|
||||||
const deviceid = res.locals.deviceid;
|
const deviceId = res.locals.deviceId;
|
||||||
|
|
||||||
// 查找当前 token 对应的应用安装记录
|
// 查找当前 token 对应的应用安装记录
|
||||||
const appInstall = await prisma.appinstall.findUnique({
|
const appInstall = await prisma.appInstall.findUnique({
|
||||||
where: { token },
|
where: { token },
|
||||||
include: {
|
include: {
|
||||||
device: {
|
device: {
|
||||||
@ -107,11 +107,11 @@ router.get(
|
|||||||
success: true,
|
success: true,
|
||||||
token: appInstall.token,
|
token: appInstall.token,
|
||||||
appId: appInstall.appId,
|
appId: appInstall.appId,
|
||||||
devicetype: appInstall.devicetype,
|
deviceType: appInstall.deviceType,
|
||||||
isreadonly: appInstall.isreadonly,
|
isReadOnly: appInstall.isReadOnly,
|
||||||
note: appInstall.note,
|
note: appInstall.note,
|
||||||
installedAt: appInstall.installedAt,
|
installedAt: appInstall.installedAt,
|
||||||
updatedat: appInstall.updatedat,
|
updatedAt: appInstall.updatedAt,
|
||||||
device: {
|
device: {
|
||||||
id: appInstall.device.id,
|
id: appInstall.device.id,
|
||||||
uuid: appInstall.device.uuid,
|
uuid: appInstall.device.uuid,
|
||||||
@ -130,7 +130,7 @@ router.get(
|
|||||||
"/_keys",
|
"/_keys",
|
||||||
tokenReadLimiter,
|
tokenReadLimiter,
|
||||||
errors.catchAsync(async (req, res) => {
|
errors.catchAsync(async (req, res) => {
|
||||||
const deviceid = res.locals.deviceid;
|
const deviceId = res.locals.deviceId;
|
||||||
const { sortBy, sortDir, limit, skip } = req.query;
|
const { sortBy, sortDir, limit, skip } = req.query;
|
||||||
|
|
||||||
// 构建选项
|
// 构建选项
|
||||||
@ -141,7 +141,7 @@ router.get(
|
|||||||
skip: skip ? parseInt(skip) : 0,
|
skip: skip ? parseInt(skip) : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const keys = await kvStore.listKeysOnly(deviceid, options);
|
const keys = await kvStore.listKeysOnly(deviceId, options);
|
||||||
const totalRows = keys.length;
|
const totalRows = keys.length;
|
||||||
|
|
||||||
// 构建响应对象
|
// 构建响应对象
|
||||||
@ -181,7 +181,7 @@ router.get(
|
|||||||
"/",
|
"/",
|
||||||
tokenReadLimiter,
|
tokenReadLimiter,
|
||||||
errors.catchAsync(async (req, res) => {
|
errors.catchAsync(async (req, res) => {
|
||||||
const deviceid = res.locals.deviceid;
|
const deviceId = res.locals.deviceId;
|
||||||
const { sortBy, sortDir, limit, skip } = req.query;
|
const { sortBy, sortDir, limit, skip } = req.query;
|
||||||
|
|
||||||
// 构建选项
|
// 构建选项
|
||||||
@ -192,8 +192,8 @@ router.get(
|
|||||||
skip: skip ? parseInt(skip) : 0,
|
skip: skip ? parseInt(skip) : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const keys = await kvStore.list(deviceid, options);
|
const keys = await kvStore.list(deviceId, options);
|
||||||
const totalRows = await kvStore.count(deviceid);
|
const totalRows = await kvStore.count(deviceId);
|
||||||
|
|
||||||
// 构建响应对象
|
// 构建响应对象
|
||||||
const response = {
|
const response = {
|
||||||
@ -227,10 +227,10 @@ router.get(
|
|||||||
"/:key",
|
"/:key",
|
||||||
tokenReadLimiter,
|
tokenReadLimiter,
|
||||||
errors.catchAsync(async (req, res, next) => {
|
errors.catchAsync(async (req, res, next) => {
|
||||||
const deviceid = res.locals.deviceid;
|
const deviceId = res.locals.deviceId;
|
||||||
const { key } = req.params;
|
const { key } = req.params;
|
||||||
|
|
||||||
const value = await kvStore.get(deviceid, key);
|
const value = await kvStore.get(deviceId, key);
|
||||||
|
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
return next(
|
return next(
|
||||||
@ -250,10 +250,10 @@ router.get(
|
|||||||
"/:key/metadata",
|
"/:key/metadata",
|
||||||
tokenReadLimiter,
|
tokenReadLimiter,
|
||||||
errors.catchAsync(async (req, res, next) => {
|
errors.catchAsync(async (req, res, next) => {
|
||||||
const deviceid = res.locals.deviceid;
|
const deviceId = res.locals.deviceId;
|
||||||
const { key } = req.params;
|
const { key } = req.params;
|
||||||
|
|
||||||
const metadata = await kvStore.getMetadata(deviceid, key);
|
const metadata = await kvStore.getMetadata(deviceId, key);
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
return next(
|
return next(
|
||||||
errors.createError(404, `未找到键名为 '${key}' 的记录`)
|
errors.createError(404, `未找到键名为 '${key}' 的记录`)
|
||||||
@ -272,11 +272,11 @@ router.post(
|
|||||||
tokenBatchLimiter,
|
tokenBatchLimiter,
|
||||||
errors.catchAsync(async (req, res, next) => {
|
errors.catchAsync(async (req, res, next) => {
|
||||||
// 检查token是否为只读
|
// 检查token是否为只读
|
||||||
if (res.locals.appInstall?.isreadonly) {
|
if (res.locals.appInstall?.isReadOnly) {
|
||||||
return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
|
return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceid = res.locals.deviceid;
|
const deviceId = res.locals.deviceId;
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
|
|
||||||
if (!data || Object.keys(data).length === 0) {
|
if (!data || Object.keys(data).length === 0) {
|
||||||
@ -289,7 +289,7 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取客户端IP
|
// 获取客户端IP
|
||||||
const creatorip =
|
const creatorIp =
|
||||||
req.headers["x-forwarded-for"] ||
|
req.headers["x-forwarded-for"] ||
|
||||||
req.connection.remoteAddress ||
|
req.connection.remoteAddress ||
|
||||||
req.socket.remoteAddress ||
|
req.socket.remoteAddress ||
|
||||||
@ -297,13 +297,13 @@ router.post(
|
|||||||
"";
|
"";
|
||||||
|
|
||||||
// 使用优化的批量upsert方法
|
// 使用优化的批量upsert方法
|
||||||
const { results, errors: errorList } = await kvStore.batchUpsert(deviceid, data, creatorip);
|
const { results, errors: errorList } = await kvStore.batchUpsert(deviceId, data, creatorIp);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
code: 200,
|
code: 200,
|
||||||
message: "批量导入成功",
|
message: "批量导入成功",
|
||||||
data: {
|
data: {
|
||||||
deviceid,
|
deviceId,
|
||||||
summary: {
|
summary: {
|
||||||
total: Object.keys(data).length,
|
total: Object.keys(data).length,
|
||||||
successful: results.length,
|
successful: results.length,
|
||||||
@ -328,11 +328,11 @@ router.post(
|
|||||||
tokenWriteLimiter,
|
tokenWriteLimiter,
|
||||||
errors.catchAsync(async (req, res, next) => {
|
errors.catchAsync(async (req, res, next) => {
|
||||||
// 检查token是否为只读
|
// 检查token是否为只读
|
||||||
if (res.locals.appInstall?.isreadonly) {
|
if (res.locals.appInstall?.isReadOnly) {
|
||||||
return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
|
return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceid = res.locals.deviceid;
|
const deviceId = res.locals.deviceId;
|
||||||
const { key } = req.params;
|
const { key } = req.params;
|
||||||
let value = req.body;
|
let value = req.body;
|
||||||
|
|
||||||
@ -351,14 +351,14 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取客户端IP
|
// 获取客户端IP
|
||||||
const creatorip =
|
const creatorIp =
|
||||||
req.headers["x-forwarded-for"] ||
|
req.headers["x-forwarded-for"] ||
|
||||||
req.connection.remoteAddress ||
|
req.connection.remoteAddress ||
|
||||||
req.socket.remoteAddress ||
|
req.socket.remoteAddress ||
|
||||||
req.connection.socket?.remoteAddress ||
|
req.connection.socket?.remoteAddress ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
const result = await kvStore.upsert(deviceid, key, value, creatorip);
|
const result = await kvStore.upsert(deviceId, key, value, creatorIp);
|
||||||
|
|
||||||
// 广播单个键的变更
|
// 广播单个键的变更
|
||||||
const uuid = res.locals.device?.uuid;
|
const uuid = res.locals.device?.uuid;
|
||||||
@ -366,16 +366,16 @@ router.post(
|
|||||||
broadcastKeyChanged(uuid, {
|
broadcastKeyChanged(uuid, {
|
||||||
key: result.key,
|
key: result.key,
|
||||||
action: "upsert",
|
action: "upsert",
|
||||||
created: result.createdat.getTime() === result.updatedat.getTime(),
|
created: result.createdAt.getTime() === result.updatedAt.getTime(),
|
||||||
updatedat: result.updatedat,
|
updatedAt: result.updatedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
deviceid: result.deviceid,
|
deviceId: result.deviceId,
|
||||||
key: result.key,
|
key: result.key,
|
||||||
created: result.createdat.getTime() === result.updatedat.getTime(),
|
created: result.createdAt.getTime() === result.updatedAt.getTime(),
|
||||||
updatedat: result.updatedat,
|
updatedAt: result.updatedAt,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -389,14 +389,14 @@ router.delete(
|
|||||||
tokenDeleteLimiter,
|
tokenDeleteLimiter,
|
||||||
errors.catchAsync(async (req, res, next) => {
|
errors.catchAsync(async (req, res, next) => {
|
||||||
// 检查token是否为只读
|
// 检查token是否为只读
|
||||||
if (res.locals.appInstall?.isreadonly) {
|
if (res.locals.appInstall?.isReadOnly) {
|
||||||
return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
|
return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceid = res.locals.deviceid;
|
const deviceId = res.locals.deviceId;
|
||||||
const { key } = req.params;
|
const { key } = req.params;
|
||||||
|
|
||||||
const result = await kvStore.delete(deviceid, key);
|
const result = await kvStore.delete(deviceId, key);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
class DeviceCodeStore {
|
class DeviceCodeStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 存储结构: { deviceCode: { token: string, expiresAt: number, createdat: number } }
|
// 存储结构: { deviceCode: { token: string, expiresAt: number, createdAt: number } }
|
||||||
this.store = new Map();
|
this.store = new Map();
|
||||||
|
|
||||||
// 默认过期时间: 15分钟
|
// 默认过期时间: 15分钟
|
||||||
@ -44,7 +44,7 @@ class DeviceCodeStore {
|
|||||||
this.store.set(deviceCode, {
|
this.store.set(deviceCode, {
|
||||||
token: null,
|
token: null,
|
||||||
expiresAt: now + this.expirationTime,
|
expiresAt: now + this.expirationTime,
|
||||||
createdat: now,
|
createdAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
return deviceCode;
|
return deviceCode;
|
||||||
@ -143,7 +143,7 @@ class DeviceCodeStore {
|
|||||||
return {
|
return {
|
||||||
hasToken: !!entry.token,
|
hasToken: !!entry.token,
|
||||||
expiresAt: entry.expiresAt,
|
expiresAt: entry.expiresAt,
|
||||||
createdat: entry.createdat,
|
createdAt: entry.createdAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export function generateAccountToken(account) {
|
|||||||
provider: account.provider,
|
provider: account.provider,
|
||||||
email: account.email,
|
email: account.email,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
avatarurl: account.avatarurl,
|
avatarUrl: account.avatarUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
174
utils/kvStore.js
174
utils/kvStore.js
@ -1,18 +1,18 @@
|
|||||||
import {prisma} from "./prisma.js";
|
|
||||||
import {keysTotal} from "./metrics.js";
|
import {keysTotal} from "./metrics.js";
|
||||||
|
import { prisma } from "./prisma.js";
|
||||||
|
|
||||||
class KVStore {
|
class KVStore {
|
||||||
/**
|
/**
|
||||||
* 通过设备ID和键名获取值
|
* 通过设备ID和键名获取值
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
* @param {string} key - 键名
|
* @param {string} key - 键名
|
||||||
* @returns {object|null} 键对应的值或null
|
* @returns {object|null} 键对应的值或null
|
||||||
*/
|
*/
|
||||||
async get(deviceid, key) {
|
async get(deviceId, key) {
|
||||||
const item = await prisma.kvstore.findUnique({
|
const item = await prisma.kVStore.findUnique({
|
||||||
where: {
|
where: {
|
||||||
deviceid_key: {
|
deviceId_key: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
key: key,
|
key: key,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -22,24 +22,24 @@ class KVStore {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取键的完整信息(包括元数据)
|
* 获取键的完整信息(包括元数据)
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
* @param {string} key - 键名
|
* @param {string} key - 键名
|
||||||
* @returns {object|null} 键的完整信息或null
|
* @returns {object|null} 键的完整信息或null
|
||||||
*/
|
*/
|
||||||
async getMetadata(deviceid, key) {
|
async getMetadata(deviceId, key) {
|
||||||
const item = await prisma.kvstore.findUnique({
|
const item = await prisma.kVStore.findUnique({
|
||||||
where: {
|
where: {
|
||||||
deviceid_key: {
|
deviceId_key: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
key: key,
|
key: key,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
key: true,
|
key: true,
|
||||||
deviceid: true,
|
deviceId: true,
|
||||||
creatorip: true,
|
creatorIp: true,
|
||||||
createdat: true,
|
createdAt: true,
|
||||||
updatedat: true,
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -47,67 +47,67 @@ class KVStore {
|
|||||||
|
|
||||||
// 转换为更友好的格式
|
// 转换为更友好的格式
|
||||||
return {
|
return {
|
||||||
deviceid: item.deviceid,
|
deviceId: item.deviceId,
|
||||||
key: item.key,
|
key: item.key,
|
||||||
metadata: {
|
metadata: {
|
||||||
creatorip: item.creatorip,
|
creatorIp: item.creatorIp,
|
||||||
createdat: item.createdat,
|
createdAt: item.createdAt,
|
||||||
updatedat: item.updatedat,
|
updatedAt: item.updatedAt,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在指定设备下创建或更新键值
|
* 在指定设备下创建或更新键值
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
* @param {string} key - 键名
|
* @param {string} key - 键名
|
||||||
* @param {object} value - 键值
|
* @param {object} value - 键值
|
||||||
* @param {string} creatorip - 创建者IP,可选
|
* @param {string} creatorIp - 创建者IP,可选
|
||||||
* @returns {object} 创建或更新的记录
|
* @returns {object} 创建或更新的记录
|
||||||
*/
|
*/
|
||||||
async upsert(deviceid, key, value, creatorip = "") {
|
async upsert(deviceId, key, value, creatorIp = "") {
|
||||||
const item = await prisma.kvstore.upsert({
|
const item = await prisma.kVStore.upsert({
|
||||||
where: {
|
where: {
|
||||||
deviceid_key: {
|
deviceId_key: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
key: key,
|
key: key,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
value,
|
value,
|
||||||
...(creatorip && {creatorip}),
|
...(creatorIp && {creatorIp}),
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
key: key,
|
key: key,
|
||||||
value,
|
value,
|
||||||
creatorip: creatorip,
|
creatorIp,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新键总数指标
|
// 更新键总数指标
|
||||||
const totalKeys = await prisma.kvstore.count();
|
const totalKeys = await prisma.kVStore.count();
|
||||||
keysTotal.set(totalKeys);
|
keysTotal.set(totalKeys);
|
||||||
|
|
||||||
// 返回带有设备ID和原始键的结果
|
// 返回带有设备ID和原始键的结果
|
||||||
return {
|
return {
|
||||||
deviceid,
|
deviceId,
|
||||||
key,
|
key,
|
||||||
value: item.value,
|
value: item.value,
|
||||||
creatorip: item.creatorip,
|
creatorIp: item.creatorIp,
|
||||||
createdat: item.createdat,
|
createdAt: item.createdAt,
|
||||||
updatedat: item.updatedat,
|
updatedAt: item.updatedAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量创建或更新键值对(优化性能)
|
* 批量创建或更新键值对(优化性能)
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
* @param {object} data - 键值对数据 {key1: value1, key2: value2, ...}
|
* @param {object} data - 键值对数据 {key1: value1, key2: value2, ...}
|
||||||
* @param {string} creatorip - 创建者IP,可选
|
* @param {string} creatorIp - 创建者IP,可选
|
||||||
* @returns {object} {results: Array, errors: Array}
|
* @returns {object} {results: Array, errors: Array}
|
||||||
*/
|
*/
|
||||||
async batchUpsert(deviceid, data, creatorip = "") {
|
async batchUpsert(deviceId, data, creatorIp = "") {
|
||||||
const results = [];
|
const results = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
@ -115,30 +115,30 @@ class KVStore {
|
|||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
try {
|
try {
|
||||||
const item = await tx.kvstore.upsert({
|
const item = await tx.kVStore.upsert({
|
||||||
where: {
|
where: {
|
||||||
deviceid_key: {
|
deviceId_key: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
key: key,
|
key: key,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
value,
|
value,
|
||||||
...(creatorip && {creatorip: creatorip}),
|
...(creatorIp && {creatorIp}),
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
key: key,
|
key: key,
|
||||||
value,
|
value,
|
||||||
creatorip: creatorip,
|
creatorIp,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
key: item.key,
|
key: item.key,
|
||||||
created: item.createdat.getTime() === item.updatedat.getTime(),
|
created: item.createdAt.getTime() === item.updatedAt.getTime(),
|
||||||
createdat: item.createdat,
|
createdAt: item.createdAt,
|
||||||
updatedat: item.updatedat,
|
updatedAt: item.updatedAt,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push({
|
errors.push({
|
||||||
@ -150,7 +150,7 @@ class KVStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 在事务完成后,一次性更新指标
|
// 在事务完成后,一次性更新指标
|
||||||
const totalKeys = await prisma.kvstore.count();
|
const totalKeys = await prisma.kVStore.count();
|
||||||
keysTotal.set(totalKeys);
|
keysTotal.set(totalKeys);
|
||||||
|
|
||||||
return { results, errors };
|
return { results, errors };
|
||||||
@ -158,26 +158,26 @@ class KVStore {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过设备ID和键名删除
|
* 通过设备ID和键名删除
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
* @param {string} key - 键名
|
* @param {string} key - 键名
|
||||||
* @returns {object|null} 删除的记录或null
|
* @returns {object|null} 删除的记录或null
|
||||||
*/
|
*/
|
||||||
async delete(deviceid, key) {
|
async delete(deviceId, key) {
|
||||||
try {
|
try {
|
||||||
const item = await prisma.kvstore.delete({
|
const item = await prisma.kVStore.delete({
|
||||||
where: {
|
where: {
|
||||||
deviceid_key: {
|
deviceId_key: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
key: key,
|
key: key,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新键总数指标
|
// 更新键总数指标
|
||||||
const totalKeys = await prisma.kvstore.count();
|
const totalKeys = await prisma.kVStore.count();
|
||||||
keysTotal.set(totalKeys);
|
keysTotal.set(totalKeys);
|
||||||
|
|
||||||
return item ? {...item, deviceid, key} : null;
|
return item ? {...item, deviceId, key} : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 忽略记录不存在的错误
|
// 忽略记录不存在的错误
|
||||||
if (error.code === "P2025") {
|
if (error.code === "P2025") {
|
||||||
@ -189,11 +189,11 @@ class KVStore {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 列出指定设备下的所有键名及其元数据
|
* 列出指定设备下的所有键名及其元数据
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
* @param {object} options - 选项参数
|
* @param {object} options - 选项参数
|
||||||
* @returns {Array} 键名和元数据数组
|
* @returns {Array} 键名和元数据数组
|
||||||
*/
|
*/
|
||||||
async list(deviceid, options = {}) {
|
async list(deviceId, options = {}) {
|
||||||
const {sortBy = "key", sortDir = "asc", limit = 100, skip = 0} = options;
|
const {sortBy = "key", sortDir = "asc", limit = 100, skip = 0} = options;
|
||||||
|
|
||||||
// 构建排序条件
|
// 构建排序条件
|
||||||
@ -201,16 +201,16 @@ class KVStore {
|
|||||||
orderBy[sortBy] = sortDir.toLowerCase();
|
orderBy[sortBy] = sortDir.toLowerCase();
|
||||||
|
|
||||||
// 查询设备的所有键
|
// 查询设备的所有键
|
||||||
const items = await prisma.kvstore.findMany({
|
const items = await prisma.kVStore.findMany({
|
||||||
where: {
|
where: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
deviceid: true,
|
deviceId: true,
|
||||||
key: true,
|
key: true,
|
||||||
creatorip: true,
|
creatorIp: true,
|
||||||
createdat: true,
|
createdAt: true,
|
||||||
updatedat: true,
|
updatedAt: true,
|
||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
orderBy,
|
orderBy,
|
||||||
@ -220,23 +220,23 @@ class KVStore {
|
|||||||
|
|
||||||
// 处理结果
|
// 处理结果
|
||||||
return items.map((item) => ({
|
return items.map((item) => ({
|
||||||
deviceid: item.deviceid,
|
deviceId: item.deviceId,
|
||||||
key: item.key,
|
key: item.key,
|
||||||
metadata: {
|
metadata: {
|
||||||
creatorip: item.creatorip,
|
creatorIp: item.creatorIp,
|
||||||
createdat: item.createdat,
|
createdAt: item.createdAt,
|
||||||
updatedat: item.updatedat,
|
updatedAt: item.updatedAt,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定设备下的键名列表(不包括内容)
|
* 获取指定设备下的键名列表(不包括内容)
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
* @param {object} options - 查询选项
|
* @param {object} options - 查询选项
|
||||||
* @returns {Array} 键名列表
|
* @returns {Array} 键名列表
|
||||||
*/
|
*/
|
||||||
async listKeysOnly(deviceid, options = {}) {
|
async listKeysOnly(deviceId, options = {}) {
|
||||||
const {sortBy = "key", sortDir = "asc", limit = 100, skip = 0} = options;
|
const {sortBy = "key", sortDir = "asc", limit = 100, skip = 0} = options;
|
||||||
|
|
||||||
// 构建排序条件
|
// 构建排序条件
|
||||||
@ -244,9 +244,9 @@ class KVStore {
|
|||||||
orderBy[sortBy] = sortDir.toLowerCase();
|
orderBy[sortBy] = sortDir.toLowerCase();
|
||||||
|
|
||||||
// 查询设备的所有键,只选择键名
|
// 查询设备的所有键,只选择键名
|
||||||
const items = await prisma.kvstore.findMany({
|
const items = await prisma.kVStore.findMany({
|
||||||
where: {
|
where: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
key: true,
|
key: true,
|
||||||
@ -262,13 +262,13 @@ class KVStore {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计指定设备下的键值对数量
|
* 统计指定设备下的键值对数量
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
* @returns {number} 键值对数量
|
* @returns {number} 键值对数量
|
||||||
*/
|
*/
|
||||||
async count(deviceid) {
|
async count(deviceId) {
|
||||||
const count = await prisma.kvstore.count({
|
const count = await prisma.kVStore.count({
|
||||||
where: {
|
where: {
|
||||||
deviceid: deviceid,
|
deviceId: deviceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return count;
|
return count;
|
||||||
@ -276,32 +276,32 @@ class KVStore {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定设备的统计信息
|
* 获取指定设备的统计信息
|
||||||
* @param {number} deviceid - 设备ID
|
* @param {number} deviceId - 设备ID
|
||||||
* @returns {object} 统计信息
|
* @returns {object} 统计信息
|
||||||
*/
|
*/
|
||||||
async getStats(deviceid) {
|
async getStats(deviceId) {
|
||||||
const [totalKeys, oldestKey, newestKey] = await Promise.all([
|
const [totalKeys, oldestKey, newestKey] = await Promise.all([
|
||||||
prisma.kvstore.count({
|
prisma.kVStore.count({
|
||||||
where: { deviceid },
|
where: { deviceId },
|
||||||
}),
|
}),
|
||||||
prisma.kvstore.findFirst({
|
prisma.kVStore.findFirst({
|
||||||
where: { deviceid },
|
where: { deviceId },
|
||||||
orderBy: { createdat: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
select: { createdat: true, key: true },
|
select: { createdAt: true, key: true },
|
||||||
}),
|
}),
|
||||||
prisma.kvstore.findFirst({
|
prisma.kVStore.findFirst({
|
||||||
where: { deviceid },
|
where: { deviceId },
|
||||||
orderBy: { updatedat: "desc" },
|
orderBy: { updatedAt: "desc" },
|
||||||
select: { updatedat: true, key: true },
|
select: { updatedAt: true, key: true },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalKeys,
|
totalKeys,
|
||||||
oldestKey: oldestKey?.key,
|
oldestKey: oldestKey?.key,
|
||||||
oldestCreatedAt: oldestKey?.createdat,
|
oldestCreatedAt: oldestKey?.createdAt,
|
||||||
newestKey: newestKey?.key,
|
newestKey: newestKey?.key,
|
||||||
newestUpdatedAt: newestKey?.updatedat,
|
newestUpdatedAt: newestKey?.updatedAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,9 +33,8 @@ export async function initializeMetrics() {
|
|||||||
registeredDevicesTotal.set(deviceCount);
|
registeredDevicesTotal.set(deviceCount);
|
||||||
|
|
||||||
// 获取已创建键总数
|
// 获取已创建键总数
|
||||||
const keyCount = await prisma.kvstore.count();
|
const keyCount = await prisma.kVStore.count();
|
||||||
keysTotal.set(keyCount);
|
keysTotal.set(keyCount);
|
||||||
|
|
||||||
console.log('Prometheus metrics initialized - Devices:', deviceCount, 'Keys:', keyCount);
|
console.log('Prometheus metrics initialized - Devices:', deviceCount, 'Keys:', keyCount);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize metrics:', error);
|
console.error('Failed to initialize metrics:', error);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {prisma} from "./prisma.js";
|
|
||||||
import kvStore from "./kvStore.js";
|
import kvStore from "./kvStore.js";
|
||||||
|
import { prisma } from "./prisma.js";
|
||||||
|
|
||||||
// 系统保留UUID用于存储站点信息
|
// 系统保留UUID用于存储站点信息
|
||||||
const SYSTEM_DEVICE_UUID = "00000000-0000-4000-8000-000000000000";
|
const SYSTEM_DEVICE_UUID = "00000000-0000-4000-8000-000000000000";
|
||||||
@ -46,8 +46,8 @@ async function getSystemDeviceId() {
|
|||||||
*/
|
*/
|
||||||
export const initReadme = async () => {
|
export const initReadme = async () => {
|
||||||
try {
|
try {
|
||||||
const deviceid = await getSystemDeviceId();
|
const deviceId = await getSystemDeviceId();
|
||||||
const storedValue = await kvStore.get(deviceid, "info");
|
const storedValue = await kvStore.get(deviceId, "info");
|
||||||
|
|
||||||
// 合并默认值与存储值,确保结构完整
|
// 合并默认值与存储值,确保结构完整
|
||||||
readmeValue = {
|
readmeValue = {
|
||||||
@ -82,8 +82,8 @@ export const getReadmeValue = () => {
|
|||||||
*/
|
*/
|
||||||
export const updateReadmeValue = async (newValue) => {
|
export const updateReadmeValue = async (newValue) => {
|
||||||
try {
|
try {
|
||||||
const deviceid = await getSystemDeviceId();
|
const deviceId = await getSystemDeviceId();
|
||||||
await kvStore.upsert(deviceid, "info", newValue);
|
await kvStore.upsert(deviceId, "info", newValue);
|
||||||
readmeValue = {
|
readmeValue = {
|
||||||
...defaultReadme,
|
...defaultReadme,
|
||||||
...newValue,
|
...newValue,
|
||||||
|
|||||||
@ -13,10 +13,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import { prisma } from "./prisma.js";
|
|
||||||
import { onlineDevicesGauge } from "./metrics.js";
|
import { onlineDevicesGauge } from "./metrics.js";
|
||||||
import DeviceDetector from "node-device-detector";
|
import DeviceDetector from "node-device-detector";
|
||||||
import ClientHints from "node-device-detector/client-hints.js";
|
import ClientHints from "node-device-detector/client-hints.js";
|
||||||
|
import { prisma } from "./prisma.js";
|
||||||
|
|
||||||
// Socket.IO 单例实例
|
// Socket.IO 单例实例
|
||||||
let io = null;
|
let io = null;
|
||||||
@ -33,7 +33,7 @@ const clientHints = new ClientHints();
|
|||||||
const onlineMap = new Map();
|
const onlineMap = new Map();
|
||||||
// 在线 token 映射:token -> Set<socketId> (用于指标统计)
|
// 在线 token 映射:token -> Set<socketId> (用于指标统计)
|
||||||
const onlineTokens = new Map();
|
const onlineTokens = new Map();
|
||||||
// 令牌信息缓存:token -> {appId, isreadonly, devicetype, note, deviceUuid, deviceName}
|
// 令牌信息缓存:token -> {appId, isReadOnly, deviceType, note, deviceUuid, deviceName}
|
||||||
const tokenInfoCache = new Map();
|
const tokenInfoCache = new Map();
|
||||||
// 事件历史记录:每个设备最多保存1000条事件记录
|
// 事件历史记录:每个设备最多保存1000条事件记录
|
||||||
const eventHistory = new Map(); // uuid -> Array<EventRecord>
|
const eventHistory = new Map(); // uuid -> Array<EventRecord>
|
||||||
@ -132,7 +132,7 @@ export function initSocket(server) {
|
|||||||
try {
|
try {
|
||||||
const token = payload?.token || payload?.apptoken;
|
const token = payload?.token || payload?.apptoken;
|
||||||
if (typeof token !== "string" || token.length === 0) return;
|
if (typeof token !== "string" || token.length === 0) return;
|
||||||
const appInstall = await prisma.appinstall.findUnique({
|
const appInstall = await prisma.appInstall.findUnique({
|
||||||
where: { token },
|
where: { token },
|
||||||
include: { device: { select: { uuid: true } } },
|
include: { device: { select: { uuid: true } } },
|
||||||
});
|
});
|
||||||
@ -175,9 +175,9 @@ export function initSocket(server) {
|
|||||||
devices: historyData,
|
devices: historyData,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
requestedBy: {
|
requestedBy: {
|
||||||
devicetype: socket.data.tokenInfo?.devicetype,
|
deviceType: socket.data.tokenInfo?.deviceType,
|
||||||
deviceName: socket.data.tokenInfo?.deviceName,
|
deviceName: socket.data.tokenInfo?.deviceName,
|
||||||
isreadonly: socket.data.tokenInfo?.isreadonly
|
isReadOnly: socket.data.tokenInfo?.isReadOnly
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -219,7 +219,7 @@ export function initSocket(server) {
|
|||||||
|
|
||||||
// 检查只读权限
|
// 检查只读权限
|
||||||
const tokenInfo = socket.data.tokenInfo;
|
const tokenInfo = socket.data.tokenInfo;
|
||||||
if (tokenInfo?.isreadonly) {
|
if (tokenInfo?.isReadOnly) {
|
||||||
socket.emit("event-error", { reason: "readonly_token_cannot_send_events" });
|
socket.emit("event-error", { reason: "readonly_token_cannot_send_events" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -243,9 +243,9 @@ export function initSocket(server) {
|
|||||||
senderId: socket.id,
|
senderId: socket.id,
|
||||||
senderInfo: {
|
senderInfo: {
|
||||||
appId: tokenInfo?.appId,
|
appId: tokenInfo?.appId,
|
||||||
devicetype: tokenInfo?.devicetype,
|
deviceType: tokenInfo?.deviceType,
|
||||||
deviceName: tokenInfo?.note,
|
deviceName: tokenInfo?.note,
|
||||||
isreadonly: tokenInfo?.isreadonly || false,
|
isReadOnly: tokenInfo?.isReadOnly || false,
|
||||||
note: tokenInfo?.note
|
note: tokenInfo?.note
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -385,7 +385,7 @@ function removeTokenConnection(token, socketId) {
|
|||||||
/**
|
/**
|
||||||
* 广播某设备下 KV 键已变更
|
* 广播某设备下 KV 键已变更
|
||||||
* @param {string} uuid 设备 uuid
|
* @param {string} uuid 设备 uuid
|
||||||
* @param {object} payload { key, action: 'upsert'|'delete'|'batch', updatedat?, created? }
|
* @param {object} payload { key, action: 'upsert'|'delete'|'batch', updatedAt?, created? }
|
||||||
*/
|
*/
|
||||||
export function broadcastKeyChanged(uuid, payload) {
|
export function broadcastKeyChanged(uuid, payload) {
|
||||||
if (!io || !uuid) return;
|
if (!io || !uuid) return;
|
||||||
@ -400,9 +400,9 @@ export function broadcastKeyChanged(uuid, payload) {
|
|||||||
senderId: "realtime",
|
senderId: "realtime",
|
||||||
senderInfo: {
|
senderInfo: {
|
||||||
appId: "5c2a54d553951a37b47066ead68c8642",
|
appId: "5c2a54d553951a37b47066ead68c8642",
|
||||||
devicetype: "server",
|
deviceType: "server",
|
||||||
deviceName: "realtime",
|
deviceName: "realtime",
|
||||||
isreadonly: false,
|
isReadOnly: false,
|
||||||
note: "Database realtime sync"
|
note: "Database realtime sync"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -443,9 +443,9 @@ export function broadcastDeviceEvent(uuid, type, content = null, senderId = "sys
|
|||||||
senderId,
|
senderId,
|
||||||
senderInfo: {
|
senderInfo: {
|
||||||
appId: "system",
|
appId: "system",
|
||||||
devicetype: "system",
|
deviceType: "system",
|
||||||
deviceName: "System",
|
deviceName: "System",
|
||||||
isreadonly: false,
|
isReadOnly: false,
|
||||||
note: "System broadcast"
|
note: "System broadcast"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -549,7 +549,7 @@ export default {
|
|||||||
*/
|
*/
|
||||||
async function joinByToken(socket, token) {
|
async function joinByToken(socket, token) {
|
||||||
try {
|
try {
|
||||||
const appInstall = await prisma.appinstall.findUnique({
|
const appInstall = await prisma.appInstall.findUnique({
|
||||||
where: { token },
|
where: { token },
|
||||||
include: {
|
include: {
|
||||||
device: {
|
device: {
|
||||||
@ -576,8 +576,8 @@ async function joinByToken(socket, token) {
|
|||||||
// 缓存令牌信息,使用拼接后的设备名称
|
// 缓存令牌信息,使用拼接后的设备名称
|
||||||
const tokenInfo = {
|
const tokenInfo = {
|
||||||
appId: appInstall.appId,
|
appId: appInstall.appId,
|
||||||
isreadonly: appInstall.isreadonly,
|
isReadOnly: appInstall.isReadOnly,
|
||||||
devicetype: appInstall.devicetype,
|
deviceType: appInstall.deviceType,
|
||||||
note: appInstall.note,
|
note: appInstall.note,
|
||||||
deviceUuid: uuid,
|
deviceUuid: uuid,
|
||||||
deviceName: finalDeviceName, // 使用拼接后的设备名称
|
deviceName: finalDeviceName, // 使用拼接后的设备名称
|
||||||
@ -600,8 +600,8 @@ async function joinByToken(socket, token) {
|
|||||||
uuid,
|
uuid,
|
||||||
token,
|
token,
|
||||||
tokenInfo: {
|
tokenInfo: {
|
||||||
isreadonly: tokenInfo.isreadonly,
|
isReadOnly: tokenInfo.isReadOnly,
|
||||||
devicetype: tokenInfo.devicetype,
|
deviceType: tokenInfo.deviceType,
|
||||||
deviceName: tokenInfo.deviceName,
|
deviceName: tokenInfo.deviceName,
|
||||||
userAgent: userAgent
|
userAgent: userAgent
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import {prisma} from './prisma.js';
|
import { prisma } from './prisma.js';
|
||||||
|
|
||||||
// Token 配置
|
// Token 配置
|
||||||
const ACCESS_TOKEN_SECRET = process.env.JWT_SECRET || 'your-access-token-secret-change-this-in-production';
|
const ACCESS_TOKEN_SECRET = process.env.JWT_SECRET || 'your-access-token-secret-change-this-in-production';
|
||||||
@ -50,8 +50,8 @@ export function generateAccessToken(account) {
|
|||||||
provider: account.provider,
|
provider: account.provider,
|
||||||
email: account.email,
|
email: account.email,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
avatarurl: account.avatarurl,
|
avatarUrl: account.avatarUrl,
|
||||||
tokenversion: account.tokenversion || 1,
|
tokenVersion: account.tokenVersion || 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
return jwt.sign(payload, signKey, {
|
return jwt.sign(payload, signKey, {
|
||||||
@ -71,7 +71,7 @@ export function generateRefreshToken(account) {
|
|||||||
const payload = {
|
const payload = {
|
||||||
type: 'refresh',
|
type: 'refresh',
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
tokenversion: account.tokenversion || 1,
|
tokenVersion: account.tokenVersion || 1,
|
||||||
// 添加随机字符串增加安全性
|
// 添加随机字符串增加安全性
|
||||||
jti: crypto.randomBytes(16).toString('hex'),
|
jti: crypto.randomBytes(16).toString('hex'),
|
||||||
};
|
};
|
||||||
@ -134,39 +134,39 @@ export function verifyRefreshToken(token) {
|
|||||||
* 生成令牌对(访问令牌 + 刷新令牌)
|
* 生成令牌对(访问令牌 + 刷新令牌)
|
||||||
*/
|
*/
|
||||||
export async function generateTokenPair(account) {
|
export async function generateTokenPair(account) {
|
||||||
const accesstoken = generateAccessToken(account);
|
const accessToken = generateAccessToken(account);
|
||||||
const refreshtoken = generateRefreshToken(account);
|
const refreshToken = generateRefreshToken(account);
|
||||||
|
|
||||||
// 计算刷新令牌过期时间
|
// 计算刷新令牌过期时间
|
||||||
const refreshtokenExpiry = new Date();
|
const refreshTokenExpiry = new Date();
|
||||||
const expiresInMs = parseExpirationToMs(REFRESH_TOKEN_EXPIRES_IN);
|
const expiresInMs = parseExpirationToMs(REFRESH_TOKEN_EXPIRES_IN);
|
||||||
refreshtokenExpiry.setTime(refreshtokenExpiry.getTime() + expiresInMs);
|
refreshTokenExpiry.setTime(refreshTokenExpiry.getTime() + expiresInMs);
|
||||||
|
|
||||||
// 更新数据库中的刷新令牌
|
// 更新数据库中的刷新令牌
|
||||||
await prisma.account.update({
|
await prisma.account.update({
|
||||||
where: {id: account.id},
|
where: {id: account.id},
|
||||||
data: {
|
data: {
|
||||||
refreshtoken,
|
refreshToken,
|
||||||
refreshtokenExpiry,
|
refreshTokenExpiry,
|
||||||
updatedat: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accesstoken,
|
accessToken,
|
||||||
refreshtoken,
|
refreshToken,
|
||||||
accesstokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
|
accessTokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
|
||||||
refreshtokenExpiresIn: REFRESH_TOKEN_EXPIRES_IN,
|
refreshTokenExpiresIn: REFRESH_TOKEN_EXPIRES_IN,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新访问令牌
|
* 刷新访问令牌
|
||||||
*/
|
*/
|
||||||
export async function refreshAccessToken(refreshtoken) {
|
export async function refreshAccessToken(refreshToken) {
|
||||||
try {
|
try {
|
||||||
// 验证刷新令牌
|
// 验证刷新令牌
|
||||||
const decoded = verifyRefreshToken(refreshtoken);
|
const decoded = verifyRefreshToken(refreshToken);
|
||||||
|
|
||||||
// 从数据库获取账户信息
|
// 从数据库获取账户信息
|
||||||
const account = await prisma.account.findUnique({
|
const account = await prisma.account.findUnique({
|
||||||
@ -178,17 +178,17 @@ export async function refreshAccessToken(refreshtoken) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证刷新令牌是否匹配
|
// 验证刷新令牌是否匹配
|
||||||
if (account.refreshtoken !== refreshtoken) {
|
if (account.refreshToken !== refreshToken) {
|
||||||
throw new Error('Invalid refresh token');
|
throw new Error('Invalid refresh token');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证刷新令牌是否过期
|
// 验证刷新令牌是否过期
|
||||||
if (account.refreshtokenExpiry && account.refreshtokenExpiry < new Date()) {
|
if (account.refreshTokenExpiry && account.refreshTokenExpiry < new Date()) {
|
||||||
throw new Error('Refresh token expired');
|
throw new Error('Refresh token expired');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证令牌版本
|
// 验证令牌版本
|
||||||
if (account.tokenversion !== decoded.tokenversion) {
|
if (account.tokenVersion !== decoded.tokenVersion) {
|
||||||
throw new Error('Token version mismatch');
|
throw new Error('Token version mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,14 +196,14 @@ export async function refreshAccessToken(refreshtoken) {
|
|||||||
const newAccessToken = generateAccessToken(account);
|
const newAccessToken = generateAccessToken(account);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accesstoken: newAccessToken,
|
accessToken: newAccessToken,
|
||||||
accesstokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
|
accessTokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
|
||||||
account: {
|
account: {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
provider: account.provider,
|
provider: account.provider,
|
||||||
email: account.email,
|
email: account.email,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
avatarurl: account.avatarurl,
|
avatarUrl: account.avatarUrl,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -218,10 +218,10 @@ export async function revokeAllTokens(accountId) {
|
|||||||
await prisma.account.update({
|
await prisma.account.update({
|
||||||
where: {id: accountId},
|
where: {id: accountId},
|
||||||
data: {
|
data: {
|
||||||
tokenversion: {increment: 1},
|
tokenVersion: {increment: 1},
|
||||||
refreshtoken: null,
|
refreshToken: null,
|
||||||
refreshtokenExpiry: null,
|
refreshTokenExpiry: null,
|
||||||
updatedat: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -233,9 +233,9 @@ export async function revokeRefreshToken(accountId) {
|
|||||||
await prisma.account.update({
|
await prisma.account.update({
|
||||||
where: {id: accountId},
|
where: {id: accountId},
|
||||||
data: {
|
data: {
|
||||||
refreshtoken: null,
|
refreshToken: null,
|
||||||
refreshtokenExpiry: null,
|
refreshTokenExpiry: null,
|
||||||
updatedat: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -283,7 +283,7 @@ export async function validateAccountToken(decoded) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证令牌版本
|
// 验证令牌版本
|
||||||
if (account.tokenversion !== decoded.tokenversion) {
|
if (account.tokenVersion !== decoded.tokenVersion) {
|
||||||
throw new Error('Token version mismatch');
|
throw new Error('Token version mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user