mirror of
https://github.com/ZeroCatDev/ClassworksKV.git
synced 2026-02-04 07:44:40 +00:00
Compare commits
No commits in common. "main" and "v1.3.11" have entirely different histories.
4
.gitignore
vendored
4
.gitignore
vendored
@ -145,3 +145,7 @@ dist
|
||||
|
||||
prisma/database/data
|
||||
data/
|
||||
|
||||
/generated/prisma
|
||||
|
||||
/generated/prisma
|
||||
|
||||
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@ -1,8 +0,0 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
12
.idea/FixClassworksKV.iml
generated
12
.idea/FixClassworksKV.iml
generated
@ -1,12 +0,0 @@
|
||||
<?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
8
.idea/modules.xml
generated
@ -1,8 +0,0 @@
|
||||
<?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
6
.idea/vcs.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
276
API_AUTOAUTH.md
276
API_AUTOAUTH.md
@ -1,276 +0,0 @@
|
||||
# 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 管理接口
|
||||
@ -1,4 +1,4 @@
|
||||
FROM node:22-alpine
|
||||
FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
# 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完成
|
||||
- [ ] 单元测试通过
|
||||
- [ ] 集成测试通过
|
||||
- [ ] 性能测试通过
|
||||
|
||||
### 部署时
|
||||
- [ ] 数据库迁移执行
|
||||
- [ ] 环境变量配置
|
||||
- [ ] 服务重启验证
|
||||
- [ ] 健康检查通过
|
||||
|
||||
### 部署后
|
||||
- [ ] 新用户登录测试
|
||||
- [ ] 现有用户功能正常
|
||||
- [ ] 监控指标正常
|
||||
- [ ] 错误日志检查
|
||||
|
||||
## 🔄 回滚计划
|
||||
|
||||
### 紧急回滚
|
||||
- [ ] 回滚代码到上一版本
|
||||
- [ ] 恢复原环境变量
|
||||
- [ ] 数据库回滚方案(如需要)
|
||||
|
||||
### 数据迁移回滚
|
||||
- [ ] 备份新增字段数据
|
||||
- [ ] 移除新增字段的迁移脚本
|
||||
- [ ] 验证旧版功能正常
|
||||
|
||||
---
|
||||
|
||||
**检查完成人员**: ___________
|
||||
**检查完成时间**: ___________
|
||||
**环境**: [ ] 开发 [ ] 测试 [ ] 生产
|
||||
@ -1,489 +0,0 @@
|
||||
# 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逻辑
|
||||
@ -1,112 +0,0 @@
|
||||
# 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`
|
||||
@ -1,174 +0,0 @@
|
||||
# 账户登录密钥系统重构完成报告
|
||||
|
||||
## 📋 项目概述
|
||||
|
||||
已成功重构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
565
SOCKET_API.md
@ -1,565 +0,0 @@
|
||||
# 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: {
|
||||
clientId: process.env.ZEROCAT_CLIENT_ID,
|
||||
clientSecret: process.env.ZEROCAT_CLIENT_SECRET,
|
||||
authorizationURL: "https://zerocat-api.houlang.cloud/oauth/authorize",
|
||||
tokenURL: "https://zerocat-api.houlang.cloud/oauth/token",
|
||||
userInfoURL: "https://zerocat-api.houlang.cloud/oauth/userinfo",
|
||||
authorizationURL: "https://api.zcservice.houlang.cloud/oauth/authorize",
|
||||
tokenURL: "https://api.zcservice.houlang.cloud/oauth/token",
|
||||
userInfoURL: "https://api.zcservice.houlang.cloud/oauth/userinfo",
|
||||
scope: "user:basic user:email",
|
||||
// 展示相关
|
||||
name: "ZeroCat",
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
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]) // 同一设备的密码必须唯一
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
|
||||
/* !!! 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
|
||||
@ -1,66 +0,0 @@
|
||||
|
||||
/* !!! 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
|
||||
@ -1,502 +0,0 @@
|
||||
|
||||
/* !!! 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
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
|
||||
/* !!! 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 {}
|
||||
@ -1,232 +0,0 @@
|
||||
|
||||
/* !!! 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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,197 +0,0 @@
|
||||
|
||||
/* !!! 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]
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
|
||||
/* !!! 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'
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -6,24 +6,25 @@
|
||||
* 2. deviceInfoMiddleware - 仅获取设备信息,不创建新设备
|
||||
* 3. passwordMiddleware - 验证设备密码
|
||||
*/
|
||||
|
||||
import {prisma} from "../utils/prisma.js";
|
||||
import errors from "../utils/errors.js";
|
||||
import {verifyDevicePassword} from "../utils/crypto.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 {
|
||||
// 创建默认的自动授权配置:不需要密码、类型是classroom(一体机)
|
||||
await prisma.autoAuth.create({
|
||||
data: {
|
||||
deviceId: deviceId,
|
||||
deviceid: deviceid,
|
||||
password: null, // 无密码
|
||||
deviceType: "classroom", // 一体机类型
|
||||
isReadOnly: false, // 非只读
|
||||
devicetype: "classroom", // 一体机类型
|
||||
isreadonly: false, // 非只读
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@ -58,7 +59,7 @@ export const deviceMiddleware = errors.catchAsync(async (req, res, next) => {
|
||||
if (!device) {
|
||||
// 设备不存在,自动创建并生成智能设备名称
|
||||
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;
|
||||
|
||||
// 生成设备名称,确保不为空
|
||||
@ -69,7 +70,7 @@ export const deviceMiddleware = errors.catchAsync(async (req, res, next) => {
|
||||
uuid: deviceUuid,
|
||||
name: deviceName,
|
||||
password: null,
|
||||
passwordHint: null,
|
||||
passwordhint: null,
|
||||
accountId: null,
|
||||
},
|
||||
});
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
|
||||
import {generateAccessToken, validateAccountToken, verifyAccessToken} from "../utils/tokenManager.js";
|
||||
import {verifyToken} from "../utils/jwt.js";
|
||||
import {prisma} from "../utils/prisma.js";
|
||||
import errors from "../utils/errors.js";
|
||||
import { prisma } from "../utils/prisma.js";
|
||||
|
||||
/**
|
||||
* 新的JWT认证中间件(支持refresh token系统)
|
||||
|
||||
@ -4,8 +4,9 @@
|
||||
* 仅验证app token,设置设备和应用信息到res.locals
|
||||
* 适用于所有KV相关的接口
|
||||
*/
|
||||
|
||||
import {prisma} from "../utils/prisma.js";
|
||||
import errors from "../utils/errors.js";
|
||||
import { prisma } from "../utils/prisma.js";
|
||||
|
||||
/**
|
||||
* KV Token认证中间件
|
||||
@ -21,7 +22,7 @@ export const kvTokenAuth = async (req, res, next) => {
|
||||
}
|
||||
|
||||
// 查找token对应的应用安装信息
|
||||
const appInstall = await prisma.appInstall.findUnique({
|
||||
const appInstall = await prisma.appinstall.findUnique({
|
||||
where: {token},
|
||||
include: {
|
||||
device: true,
|
||||
@ -35,7 +36,7 @@ export const kvTokenAuth = async (req, res, next) => {
|
||||
// 将信息存储到res.locals供后续使用
|
||||
res.locals.device = appInstall.device;
|
||||
res.locals.appInstall = appInstall;
|
||||
res.locals.deviceId = appInstall.device.id;
|
||||
res.locals.deviceid = appInstall.device.id;
|
||||
res.locals.token = token;
|
||||
next();
|
||||
} catch (error) {
|
||||
|
||||
@ -5,10 +5,11 @@
|
||||
* 2. 验证密码或账户JWT(二选一)
|
||||
* 3. 适用于需要设备上下文的接口
|
||||
*/
|
||||
|
||||
import {prisma} from "../utils/prisma.js";
|
||||
import errors from "../utils/errors.js";
|
||||
import {verifyToken as verifyAccountJWT} from "../utils/jwt.js";
|
||||
import {verifyDevicePassword} from "../utils/crypto.js";
|
||||
import { prisma } from "../utils/prisma.js";
|
||||
|
||||
/**
|
||||
* UUID+密码/JWT混合认证中间件
|
||||
@ -32,7 +33,7 @@ export const uuidAuth = async (req, res, next) => {
|
||||
|
||||
// 存储设备信息到locals
|
||||
res.locals.device = device;
|
||||
res.locals.deviceId = device.id;
|
||||
res.locals.deviceid = device.id;
|
||||
|
||||
// 3. 验证密码或JWT(二选一)
|
||||
const password = extractPassword(req);
|
||||
@ -103,7 +104,7 @@ export const extractDeviceInfo = async (req, res, next) => {
|
||||
throw errors.createError(404, "设备不存在");
|
||||
}
|
||||
res.locals.device = device;
|
||||
res.locals.deviceId = device.id;
|
||||
res.locals.deviceid = device.id;
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ClassworksKV",
|
||||
"version": "1.3.15",
|
||||
"version": "1.3.11",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ClassworksKV",
|
||||
"version": "1.3.15",
|
||||
"version": "1.3.11",
|
||||
"dependencies": {
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.59.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.205.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ClassworksKV",
|
||||
"version": "1.3.15",
|
||||
"version": "1.3.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www",
|
||||
@ -32,13 +32,12 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "~1.10.0",
|
||||
"node-device-detector": "^2.2.4",
|
||||
"prom-client": "^15.1.3",
|
||||
"pg": "^8.18.0",
|
||||
"prom-client": "^15.1.3",
|
||||
"socket.io": "^4.8.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^16.5.0",
|
||||
"prisma": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,109 +2,109 @@
|
||||
CREATE SCHEMA IF NOT EXISTS "public";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Account" (
|
||||
CREATE TABLE "account" (
|
||||
"id" VARCHAR(191) NOT NULL,
|
||||
"provider" VARCHAR(191) NOT NULL,
|
||||
"providerId" VARCHAR(191) NOT NULL,
|
||||
"providerid" VARCHAR(191) NOT NULL,
|
||||
"email" VARCHAR(191),
|
||||
"name" VARCHAR(191),
|
||||
"avatarUrl" VARCHAR(191),
|
||||
"providerData" JSON,
|
||||
"accessToken" TEXT,
|
||||
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
|
||||
"refreshToken" TEXT,
|
||||
"refreshTokenExpiry" TIMESTAMPTZ(6),
|
||||
"tokenVersion" INTEGER NOT NULL DEFAULT 1,
|
||||
"avatarurl" VARCHAR(191),
|
||||
"providerdata" JSON,
|
||||
"accesstoken" TEXT,
|
||||
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedat" TIMESTAMPTZ(6) NOT NULL,
|
||||
"refreshtoken" TEXT,
|
||||
"refreshtokenexpiry" TIMESTAMPTZ(6),
|
||||
"tokenversion" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
CONSTRAINT "idx_18303_PRIMARY" PRIMARY KEY ("id")
|
||||
CONSTRAINT "idx_18048_primary" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AppInstall" (
|
||||
CREATE TABLE "appinstall" (
|
||||
"id" VARCHAR(191) NOT NULL,
|
||||
"deviceId" INTEGER NOT NULL,
|
||||
"appId" VARCHAR(191) NOT NULL,
|
||||
"deviceid" INTEGER NOT NULL,
|
||||
"appid" VARCHAR(191) NOT NULL,
|
||||
"token" VARCHAR(191) NOT NULL,
|
||||
"note" VARCHAR(191),
|
||||
"installedAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
|
||||
"deviceType" VARCHAR(191),
|
||||
"isReadOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"installedat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedat" TIMESTAMPTZ(6) NOT NULL,
|
||||
"devicetype" VARCHAR(191),
|
||||
"isreadonly" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "idx_18310_PRIMARY" PRIMARY KEY ("id")
|
||||
CONSTRAINT "idx_18055_primary" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AutoAuth" (
|
||||
CREATE TABLE "autoauth" (
|
||||
"id" VARCHAR(191) NOT NULL,
|
||||
"deviceId" INTEGER NOT NULL,
|
||||
"deviceid" INTEGER NOT NULL,
|
||||
"password" VARCHAR(191),
|
||||
"deviceType" VARCHAR(191),
|
||||
"isReadOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
|
||||
"devicetype" VARCHAR(191),
|
||||
"isreadonly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedat" TIMESTAMPTZ(6) NOT NULL,
|
||||
|
||||
CONSTRAINT "idx_18317_PRIMARY" PRIMARY KEY ("id")
|
||||
CONSTRAINT "idx_18062_primary" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Device" (
|
||||
CREATE TABLE "device" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"uuid" VARCHAR(191) NOT NULL,
|
||||
"name" VARCHAR(191),
|
||||
"accountId" VARCHAR(191),
|
||||
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
|
||||
"accountid" VARCHAR(191),
|
||||
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedat" TIMESTAMPTZ(6) NOT NULL,
|
||||
"password" VARCHAR(191),
|
||||
"passwordHint" VARCHAR(191),
|
||||
"passwordhint" VARCHAR(191),
|
||||
"namespace" VARCHAR(191),
|
||||
|
||||
CONSTRAINT "idx_18324_PRIMARY" PRIMARY KEY ("id")
|
||||
CONSTRAINT "idx_18069_primary" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "KVStore" (
|
||||
"deviceId" INTEGER NOT NULL,
|
||||
CREATE TABLE "kvstore" (
|
||||
"deviceid" INTEGER NOT NULL,
|
||||
"key" VARCHAR(191) NOT NULL,
|
||||
"value" JSON NOT NULL,
|
||||
"creatorIp" VARCHAR(191) DEFAULT '',
|
||||
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
|
||||
"creatorip" VARCHAR(191) DEFAULT '',
|
||||
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedat" TIMESTAMPTZ(6) NOT NULL,
|
||||
|
||||
CONSTRAINT "idx_18330_PRIMARY" PRIMARY KEY ("deviceId","key")
|
||||
CONSTRAINT "idx_18075_primary" PRIMARY KEY ("deviceid","key")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "idx_18303_Account_provider_providerId_key" ON "Account"("provider", "providerId");
|
||||
CREATE UNIQUE INDEX "idx_18048_account_provider_providerid_key" ON "account"("provider", "providerid");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "idx_18310_AppInstall_token_key" ON "AppInstall"("token");
|
||||
CREATE UNIQUE INDEX "idx_18055_appinstall_token_key" ON "appinstall"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_18310_AppInstall_deviceId_fkey" ON "AppInstall"("deviceId");
|
||||
CREATE INDEX "idx_18055_appinstall_deviceid_fkey" ON "appinstall"("deviceid");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "idx_18317_AutoAuth_deviceId_password_key" ON "AutoAuth"("deviceId", "password");
|
||||
CREATE UNIQUE INDEX "idx_18062_autoauth_deviceid_password_key" ON "autoauth"("deviceid", "password");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "idx_18324_Device_uuid_key" ON "Device"("uuid");
|
||||
CREATE UNIQUE INDEX "idx_18069_device_uuid_key" ON "device"("uuid");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "idx_18324_Device_namespace_key" ON "Device"("namespace");
|
||||
CREATE UNIQUE INDEX "idx_18069_device_namespace_key" ON "device"("namespace");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_18324_Device_accountId_fkey" ON "Device"("accountId");
|
||||
CREATE INDEX "idx_18069_device_accountid_fkey" ON "device"("accountid");
|
||||
|
||||
-- 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
|
||||
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
|
||||
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
|
||||
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;
|
||||
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
-- 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());
|
||||
@ -1,14 +0,0 @@
|
||||
-- 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;
|
||||
@ -1,4 +0,0 @@
|
||||
-- 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,84 +7,79 @@ datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id(map: "idx_18303_PRIMARY") @db.VarChar(191) @default(cuid())
|
||||
model account {
|
||||
id String @id(map: "idx_18048_primary") @db.VarChar(191)
|
||||
provider String @db.VarChar(191)
|
||||
providerId String @db.VarChar(191)
|
||||
providerid String @db.VarChar(191)
|
||||
email String? @db.VarChar(191)
|
||||
name String? @db.VarChar(191)
|
||||
avatarUrl String? @db.VarChar(191)
|
||||
providerData Json? @db.Json
|
||||
accessToken String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
|
||||
refreshToken String?
|
||||
refreshTokenExpiry DateTime? @db.Timestamptz(6)
|
||||
tokenVersion Int @default(1)
|
||||
avatarurl String? @db.VarChar(191)
|
||||
providerdata Json? @db.Json
|
||||
accesstoken String?
|
||||
createdat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @db.Timestamptz(6)
|
||||
updatedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @updatedAt @db.Timestamptz(6)
|
||||
refreshtoken String?
|
||||
refreshtokenexpiry DateTime? @db.Timestamptz(6)
|
||||
tokenversion Int @default(1)
|
||||
device device[]
|
||||
|
||||
devices Device[]
|
||||
|
||||
@@unique([provider, providerId], map: "idx_18303_Account_provider_providerId_key")
|
||||
@@unique([provider, providerid], map: "idx_18048_account_provider_providerid_key")
|
||||
}
|
||||
|
||||
model AppInstall {
|
||||
id String @id(map: "idx_18310_PRIMARY") @default(cuid()) @db.VarChar(191)
|
||||
deviceId Int
|
||||
appId String @db.VarChar(191)
|
||||
token String @unique(map: "idx_18310_AppInstall_token_key") @db.VarChar(191)
|
||||
model appinstall {
|
||||
id String @id(map: "idx_18055_primary") @db.VarChar(191)
|
||||
deviceid Int
|
||||
appid String @db.VarChar(191)
|
||||
token String @unique(map: "idx_18055_appinstall_token_key") @db.VarChar(191)
|
||||
note String? @db.VarChar(191)
|
||||
installedAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
|
||||
deviceType String? @db.VarChar(191)
|
||||
isReadOnly Boolean @default(false)
|
||||
installedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @db.Timestamptz(6)
|
||||
updatedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @updatedAt @db.Timestamptz(6)
|
||||
devicetype String? @db.VarChar(191)
|
||||
isreadonly Boolean @default(false)
|
||||
device device @relation(fields: [deviceid], references: [id], onDelete: Cascade)
|
||||
|
||||
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([deviceId], map: "idx_18310_AppInstall_deviceId_fkey")
|
||||
@@index([deviceid], map: "idx_18055_appinstall_deviceid_fkey")
|
||||
}
|
||||
|
||||
model AutoAuth {
|
||||
id String @id(map: "idx_18317_PRIMARY") @default(cuid()) @db.VarChar(191)
|
||||
deviceId Int
|
||||
model autoauth {
|
||||
id String @id(map: "idx_18062_primary") @db.VarChar(191)
|
||||
deviceid Int
|
||||
password String? @db.VarChar(191)
|
||||
deviceType String? @db.VarChar(191)
|
||||
isReadOnly Boolean @default(false)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
|
||||
devicetype String? @db.VarChar(191)
|
||||
isreadonly Boolean @default(false)
|
||||
createdat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @db.Timestamptz(6)
|
||||
updatedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @updatedAt @db.Timestamptz(6)
|
||||
device device @relation(fields: [deviceid], references: [id], onDelete: Cascade)
|
||||
|
||||
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([deviceId, password], map: "idx_18317_AutoAuth_deviceId_password_key")
|
||||
@@unique([deviceid, password], map: "idx_18062_autoauth_deviceid_password_key")
|
||||
}
|
||||
|
||||
model Device {
|
||||
id Int @id(map: "idx_18324_PRIMARY") @default(autoincrement())
|
||||
uuid String @unique(map: "idx_18324_Device_uuid_key") @db.VarChar(191)
|
||||
model device {
|
||||
id Int @id(map: "idx_18069_primary")
|
||||
uuid String @unique(map: "idx_18069_device_uuid_key") @db.VarChar(191)
|
||||
name String? @db.VarChar(191)
|
||||
accountId String? @db.VarChar(191)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
|
||||
accountid String? @db.VarChar(191)
|
||||
createdat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @db.Timestamptz(6)
|
||||
updatedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @updatedAt @db.Timestamptz(6)
|
||||
password String? @db.VarChar(191)
|
||||
passwordHint String? @db.VarChar(191)
|
||||
namespace String? @unique(map: "idx_18324_Device_namespace_key") @db.VarChar(191)
|
||||
passwordhint String? @db.VarChar(191)
|
||||
namespace String? @unique(map: "idx_18069_device_namespace_key") @db.VarChar(191)
|
||||
appinstall appinstall[]
|
||||
autoauth autoauth[]
|
||||
account account? @relation(fields: [accountid], references: [id])
|
||||
kvstore kvstore[]
|
||||
|
||||
// 关联关系
|
||||
account Account? @relation(fields: [accountId], references: [id], onDelete: SetNull)
|
||||
appInstalls AppInstall[]
|
||||
kvStore KVStore[] // 设备相关的KV存储
|
||||
autoAuths AutoAuth[] // 自动授权配置
|
||||
@@index([accountId], map: "idx_18324_Device_accountId_fkey")
|
||||
@@index([accountid], map: "idx_18069_device_accountid_fkey")
|
||||
}
|
||||
|
||||
model KVStore {
|
||||
deviceId Int
|
||||
model kvstore {
|
||||
deviceid Int
|
||||
key String @db.VarChar(191)
|
||||
value Json @db.Json
|
||||
creatorIp String? @default("") @db.VarChar(191)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
|
||||
creatorip String? @default("") @db.VarChar(191)
|
||||
createdat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @db.Timestamptz(6)
|
||||
updatedat DateTime @default(dbgenerated("timezone('Asia/Shanghai', now())")) @updatedAt @db.Timestamptz(6)
|
||||
device device @relation(fields: [deviceid], references: [id], onDelete: Cascade)
|
||||
|
||||
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([deviceId, key], map: "idx_18330_PRIMARY")
|
||||
@@id([deviceid, key], map: "idx_18075_primary")
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import {Router} from "express";
|
||||
import {prisma} from "../utils/prisma.js";
|
||||
import crypto from "crypto";
|
||||
import {generateState, getCallbackURL, oauthProviders} from "../config/oauth.js";
|
||||
import {generateTokenPair, refreshAccessToken, revokeAllTokens, revokeRefreshToken} from "../utils/jwt.js";
|
||||
import {jwtAuth} from "../middleware/jwt-auth.js";
|
||||
import errors from "../utils/errors.js";
|
||||
import { prisma } from "../utils/prisma.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -232,10 +232,10 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
||||
|
||||
// 2. 使用访问令牌获取用户信息
|
||||
let userResponse;
|
||||
// Casdoor 支持两种方式:Authorization Bearer 或 accessToken 查询参数
|
||||
// Casdoor 支持两种方式:Authorization Bearer 或 accesstoken 查询参数
|
||||
if (provider === 'stcn') {
|
||||
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"}});
|
||||
} else {
|
||||
userResponse = await fetch(providerConfig.userInfoURL, {
|
||||
@ -256,14 +256,14 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
||||
providerId: String(userData.id),
|
||||
email: userData.email,
|
||||
name: userData.name || userData.login,
|
||||
avatarUrl: userData.avatar_url,
|
||||
avatarurl: userData.avatar_url,
|
||||
};
|
||||
} else if (provider === "zerocat") {
|
||||
normalizedUser = {
|
||||
providerId: userData.openid,
|
||||
email: userData.email_verified ? userData.email : null,
|
||||
name: userData.nickname || userData.username,
|
||||
avatarUrl: userData.avatar,
|
||||
avatarurl: userData.avatar,
|
||||
};
|
||||
} else if (provider === "hly") {
|
||||
// 厚浪云(Logto)标准OIDC用户信息
|
||||
@ -271,7 +271,7 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
||||
providerId: userData.sub,
|
||||
email: userData.email_verified ? userData.email : null,
|
||||
name: userData.name || userData.preferred_username || userData.nickname,
|
||||
avatarUrl: userData.picture,
|
||||
avatarurl: userData.picture,
|
||||
};
|
||||
} else if (provider === "stcn") {
|
||||
// STCN(Casdoor)标准OIDC用户信息
|
||||
@ -279,7 +279,7 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
||||
providerId: userData.sub,
|
||||
email: userData.email_verified ? userData.email : userData.email || null,
|
||||
name: userData.name || userData.preferred_username || userData.nickname,
|
||||
avatarUrl: userData.picture,
|
||||
avatarurl: userData.picture,
|
||||
};
|
||||
} else if (provider === "dlass") {
|
||||
// Dlass(Casdoor)标准OIDC用户信息
|
||||
@ -287,7 +287,7 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
||||
providerId: userData.sub,
|
||||
email: userData.email_verified ? userData.email : userData.email || null,
|
||||
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: {
|
||||
email: normalizedUser.email || account.email,
|
||||
name: normalizedUser.name || account.name,
|
||||
avatarUrl: normalizedUser.avatarUrl || account.avatarUrl,
|
||||
avatarurl: normalizedUser.avatarurl || account.avatarurl,
|
||||
providerData: userData,
|
||||
//refreshToken: tokenData.refresh_token || account.refreshToken,
|
||||
updatedAt: new Date(),
|
||||
//refreshtoken: tokenData.refresh_token || account.refreshtoken,
|
||||
updatedat: new Date(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 创建新账户
|
||||
const accessToken = generateAccessToken();
|
||||
const accesstoken = generateAccessToken();
|
||||
account = await prisma.account.create({
|
||||
data: {
|
||||
provider,
|
||||
providerId: normalizedUser.providerId,
|
||||
email: normalizedUser.email,
|
||||
name: normalizedUser.name,
|
||||
avatarUrl: normalizedUser.avatarUrl,
|
||||
avatarurl: normalizedUser.avatarurl,
|
||||
providerData: userData,
|
||||
accessToken,
|
||||
//refreshToken: tokenData.refresh_token,
|
||||
accesstoken,
|
||||
//refreshtoken: tokenData.refresh_token,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -345,9 +345,9 @@ router.get("/oauth/:provider/callback", async (req, res) => {
|
||||
// 6. 重定向到前端根路径,携带JWT token
|
||||
const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
|
||||
const callbackUrl = new URL(frontendBaseUrl);
|
||||
callbackUrl.searchParams.append("access_token", tokens.accessToken);
|
||||
callbackUrl.searchParams.append("refresh_token", tokens.refreshToken);
|
||||
callbackUrl.searchParams.append("expires_in", tokens.accessTokenExpiresIn);
|
||||
callbackUrl.searchParams.append("access_token", tokens.accesstoken);
|
||||
callbackUrl.searchParams.append("refresh_token", tokens.refreshtoken);
|
||||
callbackUrl.searchParams.append("expires_in", tokens.accesstokenExpiresIn);
|
||||
callbackUrl.searchParams.append("provider", provider);
|
||||
// 附带展示信息,便于前端显示品牌与名称
|
||||
const pconf = oauthProviders[provider] || {};
|
||||
@ -392,7 +392,7 @@ router.get("/profile", jwtAuth, async (req, res, next) => {
|
||||
id: true,
|
||||
uuid: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
createdat: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -421,9 +421,9 @@ router.get("/profile", jwtAuth, async (req, res, next) => {
|
||||
providerInfo,
|
||||
email: account.email,
|
||||
name: account.name,
|
||||
avatarUrl: account.avatarUrl,
|
||||
avatarurl: account.avatarurl,
|
||||
devices: account.devices,
|
||||
createdAt: account.createdAt,
|
||||
createdat: account.createdat,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@ -487,7 +487,7 @@ router.post("/devices/bind", jwtAuth, async (req, res, next) => {
|
||||
success: true,
|
||||
message: "设备绑定成功",
|
||||
data: {
|
||||
deviceId: updatedDevice.id,
|
||||
deviceid: updatedDevice.id,
|
||||
uuid: updatedDevice.uuid,
|
||||
name: updatedDevice.name,
|
||||
},
|
||||
@ -592,8 +592,8 @@ router.get("/devices", jwtAuth, async (req, res, next) => {
|
||||
uuid: true,
|
||||
name: true,
|
||||
namespace: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
createdat: true,
|
||||
updatedat: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -627,8 +627,8 @@ router.get("/device/:uuid/account", async (req, res, next) => {
|
||||
id: true,
|
||||
provider: true,
|
||||
name: true,
|
||||
avatarUrl: true,
|
||||
createdAt: true,
|
||||
avatarurl: true,
|
||||
createdat: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -654,8 +654,8 @@ router.get("/device/:uuid/account", async (req, res, next) => {
|
||||
id: device.account.id,
|
||||
provider: device.account.provider,
|
||||
name: device.account.name,
|
||||
avatarUrl: device.account.avatarUrl,
|
||||
bindTime: device.updatedAt, // 绑定时间
|
||||
avatarurl: device.account.avatarurl,
|
||||
bindTime: device.updatedat, // 绑定时间
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@ -690,8 +690,8 @@ router.post("/refresh", async (req, res, next) => {
|
||||
success: true,
|
||||
message: "令牌刷新成功",
|
||||
data: {
|
||||
access_token: result.accessToken,
|
||||
expires_in: result.accessTokenExpiresIn,
|
||||
access_token: result.accesstoken,
|
||||
expires_in: result.accesstokenExpiresIn,
|
||||
account: result.account,
|
||||
},
|
||||
});
|
||||
@ -780,14 +780,14 @@ router.get("/token-info", jwtAuth, async (req, res, next) => {
|
||||
data: {
|
||||
accountId: account.id,
|
||||
tokenType: decoded.type || 'legacy',
|
||||
tokenVersion: decoded.tokenVersion || account.tokenVersion,
|
||||
tokenversion: decoded.tokenversion || account.tokenversion,
|
||||
issuedAt: new Date(decoded.iat * 1000),
|
||||
expiresAt: new Date(decoded.exp * 1000),
|
||||
expiresIn: expiresIn,
|
||||
isExpired: expiresIn <= 0,
|
||||
isLegacyToken: res.locals.isLegacyToken || false,
|
||||
hasRefreshToken: !!account.refreshToken,
|
||||
refreshTokenExpiry: account.refreshTokenExpiry,
|
||||
hasRefreshToken: !!account.refreshtoken,
|
||||
refreshtokenExpiry: account.refreshtokenExpiry,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import {Router} from "express";
|
||||
import {uuidAuth} from "../middleware/uuidAuth.js";
|
||||
import {prisma} from "../utils/prisma.js";
|
||||
import crypto from "crypto";
|
||||
import errors from "../utils/errors.js";
|
||||
import {verifyDevicePassword} from "../utils/crypto.js";
|
||||
import { prisma } from "../utils/prisma.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -25,15 +25,15 @@ router.get(
|
||||
return next(errors.createError(404, "设备不存在"));
|
||||
}
|
||||
|
||||
const installations = await prisma.appInstall.findMany({
|
||||
where: {deviceId: device.id},
|
||||
const installations = await prisma.appinstall.findMany({
|
||||
where: {deviceid: device.id},
|
||||
});
|
||||
|
||||
const apps = installations.map(install => ({
|
||||
appId: install.appId,
|
||||
token: install.token,
|
||||
note: install.note,
|
||||
installedAt: install.createdAt,
|
||||
installedAt: install.createdat,
|
||||
}));
|
||||
|
||||
return res.json({
|
||||
@ -60,9 +60,9 @@ router.post(
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
|
||||
// 创建安装记录
|
||||
const installation = await prisma.appInstall.create({
|
||||
const installation = await prisma.appinstall.create({
|
||||
data: {
|
||||
deviceId: device.id,
|
||||
deviceid: device.id,
|
||||
appId: appId,
|
||||
token,
|
||||
note: note || null,
|
||||
@ -75,7 +75,7 @@ router.post(
|
||||
token: installation.token,
|
||||
note: installation.note,
|
||||
name: installation.note, // 备注同时作为名称返回
|
||||
installedAt: installation.createdAt,
|
||||
installedAt: installation.createdat,
|
||||
});
|
||||
})
|
||||
);
|
||||
@ -91,7 +91,7 @@ router.delete(
|
||||
const device = res.locals.device;
|
||||
const {installId} = req.params;
|
||||
|
||||
const installation = await prisma.appInstall.findUnique({
|
||||
const installation = await prisma.appinstall.findUnique({
|
||||
where: {id: installId},
|
||||
});
|
||||
|
||||
@ -100,11 +100,11 @@ router.delete(
|
||||
}
|
||||
|
||||
// 确保安装记录属于当前设备
|
||||
if (installation.deviceId !== device.id) {
|
||||
if (installation.deviceid !== device.id) {
|
||||
return next(errors.createError(403, "无权操作此安装记录"));
|
||||
}
|
||||
|
||||
await prisma.appInstall.delete({
|
||||
await prisma.appinstall.delete({
|
||||
where: {id: installation.id},
|
||||
});
|
||||
|
||||
@ -135,8 +135,8 @@ router.get(
|
||||
}
|
||||
|
||||
// 获取该设备的所有应用安装记录(即token)
|
||||
const installations = await prisma.appInstall.findMany({
|
||||
where: {deviceId: device.id},
|
||||
const installations = await prisma.appinstall.findMany({
|
||||
where: {deviceid: device.id},
|
||||
orderBy: {installedAt: 'desc'},
|
||||
});
|
||||
|
||||
@ -235,22 +235,22 @@ router.post(
|
||||
// 根据自动授权配置创建 AppInstall
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
|
||||
const installation = await prisma.appInstall.create({
|
||||
const installation = await prisma.appinstall.create({
|
||||
data: {
|
||||
deviceId: device.id,
|
||||
deviceid: device.id,
|
||||
appId: appId,
|
||||
token,
|
||||
note: null,
|
||||
isReadOnly: matchedAutoAuth.isReadOnly,
|
||||
deviceType: matchedAutoAuth.deviceType,
|
||||
isreadonly: matchedAutoAuth.isreadonly,
|
||||
devicetype: matchedAutoAuth.devicetype,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
token: installation.token,
|
||||
deviceType: installation.deviceType,
|
||||
isReadOnly: installation.isReadOnly,
|
||||
devicetype: installation.devicetype,
|
||||
isreadonly: installation.isreadonly,
|
||||
installedAt: installation.installedAt,
|
||||
});
|
||||
})
|
||||
@ -272,7 +272,7 @@ router.post(
|
||||
}
|
||||
|
||||
// 查找 token 对应的应用安装记录
|
||||
const appInstall = await prisma.appInstall.findUnique({
|
||||
const appInstall = await prisma.appinstall.findUnique({
|
||||
where: {token},
|
||||
include: {
|
||||
device: true,
|
||||
@ -284,15 +284,15 @@ router.post(
|
||||
}
|
||||
|
||||
// 验证 token 类型是否为 student
|
||||
if (!['student', 'parent'].includes(appInstall.deviceType)) {
|
||||
if (!['student', 'parent'].includes(appInstall.devicetype)) {
|
||||
return next(errors.createError(403, "只有学生和家长类型的 token 可以设置名称"));
|
||||
}
|
||||
|
||||
// 读取设备的 classworks-list-main 键值
|
||||
const kvRecord = await prisma.kVStore.findUnique({
|
||||
const kvRecord = await prisma.kvstore.findUnique({
|
||||
where: {
|
||||
deviceId_key: {
|
||||
deviceId: appInstall.deviceId,
|
||||
deviceid_key: {
|
||||
deviceid: appInstall.deviceid,
|
||||
key: 'classworks-list-main',
|
||||
},
|
||||
},
|
||||
@ -321,17 +321,17 @@ router.post(
|
||||
}
|
||||
|
||||
// 更新 AppInstall 的 note 字段
|
||||
const updatedInstall = await prisma.appInstall.update({
|
||||
const updatedInstall = await prisma.appinstall.update({
|
||||
where: {id: appInstall.id},
|
||||
data: {note: appInstall.deviceType === 'parent' ? `${name} 家长` : name},
|
||||
data: {note: appInstall.devicetype === 'parent' ? `${name} 家长` : name},
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
token: updatedInstall.token,
|
||||
name: updatedInstall.note,
|
||||
deviceType: updatedInstall.deviceType,
|
||||
updatedAt: updatedInstall.updatedAt,
|
||||
devicetype: updatedInstall.devicetype,
|
||||
updatedat: updatedInstall.updatedat,
|
||||
});
|
||||
})
|
||||
);
|
||||
@ -352,7 +352,7 @@ router.post(
|
||||
}
|
||||
|
||||
// 查找 token 对应的应用安装记录
|
||||
const appInstall = await prisma.appInstall.findUnique({
|
||||
const appInstall = await prisma.appinstall.findUnique({
|
||||
where: {token},
|
||||
include: {
|
||||
device: true,
|
||||
@ -364,12 +364,12 @@ router.post(
|
||||
}
|
||||
|
||||
// 验证 token 类型是否为 teacher
|
||||
if (appInstall.deviceType !== 'teacher') {
|
||||
if (appInstall.devicetype !== 'teacher') {
|
||||
return next(errors.createError(403, "只有教师类型的 token 可以使用此接口"));
|
||||
}
|
||||
|
||||
// 更新 AppInstall 的 note 字段为教师名称
|
||||
const updatedInstall = await prisma.appInstall.update({
|
||||
const updatedInstall = await prisma.appinstall.update({
|
||||
where: {id: appInstall.id},
|
||||
data: {note: name},
|
||||
});
|
||||
@ -378,8 +378,8 @@ router.post(
|
||||
success: true,
|
||||
token: updatedInstall.token,
|
||||
name: updatedInstall.note,
|
||||
deviceType: updatedInstall.deviceType,
|
||||
updatedAt: updatedInstall.updatedAt,
|
||||
devicetype: updatedInstall.devicetype,
|
||||
updatedat: updatedInstall.updatedat,
|
||||
});
|
||||
})
|
||||
);
|
||||
@ -396,7 +396,7 @@ router.put(
|
||||
const {note} = req.body;
|
||||
|
||||
// 查找 token 对应的应用安装记录
|
||||
const appInstall = await prisma.appInstall.findUnique({
|
||||
const appInstall = await prisma.appinstall.findUnique({
|
||||
where: {token},
|
||||
});
|
||||
|
||||
@ -405,7 +405,7 @@ router.put(
|
||||
}
|
||||
|
||||
// 更新 AppInstall 的 note 字段
|
||||
const updatedInstall = await prisma.appInstall.update({
|
||||
const updatedInstall = await prisma.appinstall.update({
|
||||
where: {id: appInstall.id},
|
||||
data: {note: note || null},
|
||||
});
|
||||
@ -414,7 +414,7 @@ router.put(
|
||||
success: true,
|
||||
token: updatedInstall.token,
|
||||
note: updatedInstall.note,
|
||||
updatedAt: updatedInstall.updatedAt,
|
||||
updatedat: updatedInstall.updatedat,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {Router} from "express";
|
||||
import {jwtAuth} from "../middleware/jwt-auth.js";
|
||||
import {prisma} from "../utils/prisma.js";
|
||||
import errors from "../utils/errors.js";
|
||||
import { prisma } from "../utils/prisma.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -31,8 +31,8 @@ router.get(
|
||||
}
|
||||
|
||||
const autoAuths = await prisma.autoAuth.findMany({
|
||||
where: {deviceId: device.id},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
where: {deviceid: device.id},
|
||||
orderBy: {createdat: 'desc'},
|
||||
});
|
||||
|
||||
// 返回配置,智能处理密码显示
|
||||
@ -44,10 +44,10 @@ router.get(
|
||||
id: auth.id,
|
||||
password: isHashedPassword ? null : auth.password, // 哈希密码不返回
|
||||
isLegacyHash: isHashedPassword, // 标记是否为旧的哈希密码
|
||||
deviceType: auth.deviceType,
|
||||
isReadOnly: auth.isReadOnly,
|
||||
createdAt: auth.createdAt,
|
||||
updatedAt: auth.updatedAt,
|
||||
devicetype: auth.devicetype,
|
||||
isreadonly: auth.isreadonly,
|
||||
createdat: auth.createdat,
|
||||
updatedat: auth.updatedat,
|
||||
};
|
||||
});
|
||||
|
||||
@ -61,7 +61,7 @@ router.get(
|
||||
/**
|
||||
* POST /auto-auth/devices/:uuid/auth-configs
|
||||
* 创建新的自动授权配置 (需要 JWT 认证,且设备必须绑定到该账户)
|
||||
* Body: { password?: string, deviceType?: string, isReadOnly?: boolean }
|
||||
* Body: { password?: string, devicetype?: string, isreadonly?: boolean }
|
||||
*/
|
||||
router.post(
|
||||
"/devices/:uuid/auth-configs",
|
||||
@ -69,7 +69,7 @@ router.post(
|
||||
errors.catchAsync(async (req, res, next) => {
|
||||
const {uuid} = req.params;
|
||||
const account = res.locals.account;
|
||||
const {password, deviceType, isReadOnly} = req.body;
|
||||
const {password, devicetype, isreadonly} = req.body;
|
||||
|
||||
// 查找设备并验证是否属于当前账户
|
||||
const device = await prisma.device.findUnique({
|
||||
@ -85,9 +85,9 @@ router.post(
|
||||
return next(errors.createError(403, "该设备未绑定到您的账户"));
|
||||
}
|
||||
|
||||
// 验证 deviceType 如果提供的话
|
||||
// 验证 devicetype 如果提供的话
|
||||
const validDeviceTypes = ['teacher', 'student', 'classroom', 'parent'];
|
||||
if (deviceType && !validDeviceTypes.includes(deviceType)) {
|
||||
if (devicetype && !validDeviceTypes.includes(devicetype)) {
|
||||
return next(errors.createError(400, `设备类型必须是以下之一: ${validDeviceTypes.join(', ')}`));
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ router.post(
|
||||
|
||||
// 查询该设备的所有自动授权配置,本地检查是否存在相同密码
|
||||
const allAuths = await prisma.autoAuth.findMany({
|
||||
where: {deviceId: device.id},
|
||||
where: {deviceid: device.id},
|
||||
});
|
||||
|
||||
const existingAuth = allAuths.find(auth => auth.password === plainPassword);
|
||||
@ -108,10 +108,10 @@ router.post(
|
||||
// 创建新的自动授权配置(密码明文存储)
|
||||
const autoAuth = await prisma.autoAuth.create({
|
||||
data: {
|
||||
deviceId: device.id,
|
||||
deviceid: device.id,
|
||||
password: plainPassword,
|
||||
deviceType: deviceType || null,
|
||||
isReadOnly: isReadOnly || false,
|
||||
devicetype: devicetype || null,
|
||||
isreadonly: isreadonly || false,
|
||||
},
|
||||
});
|
||||
|
||||
@ -120,9 +120,9 @@ router.post(
|
||||
config: {
|
||||
id: autoAuth.id,
|
||||
password: autoAuth.password, // 返回明文密码
|
||||
deviceType: autoAuth.deviceType,
|
||||
isReadOnly: autoAuth.isReadOnly,
|
||||
createdAt: autoAuth.createdAt,
|
||||
devicetype: autoAuth.devicetype,
|
||||
isreadonly: autoAuth.isreadonly,
|
||||
createdat: autoAuth.createdat,
|
||||
},
|
||||
});
|
||||
})
|
||||
@ -130,7 +130,7 @@ router.post(
|
||||
/**
|
||||
* PUT /auto-auth/devices/:uuid/auth-configs/:configId
|
||||
* 更新自动授权配置 (需要 JWT 认证,且设备必须绑定到该账户)
|
||||
* Body: { password?: string, deviceType?: string, isReadOnly?: boolean }
|
||||
* Body: { password?: string, devicetype?: string, isreadonly?: boolean }
|
||||
*/
|
||||
router.put(
|
||||
"/devices/:uuid/auth-configs/:configId",
|
||||
@ -138,7 +138,7 @@ router.put(
|
||||
errors.catchAsync(async (req, res, next) => {
|
||||
const {uuid, configId} = req.params;
|
||||
const account = res.locals.account;
|
||||
const {password, deviceType, isReadOnly} = req.body;
|
||||
const {password, devicetype, isreadonly} = req.body;
|
||||
|
||||
// 查找设备并验证是否属于当前账户
|
||||
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, "无权操作此配置"));
|
||||
}
|
||||
|
||||
// 验证 deviceType
|
||||
// 验证 devicetype
|
||||
const validDeviceTypes = ['teacher', 'student', 'classroom', 'parent'];
|
||||
if (deviceType && !validDeviceTypes.includes(deviceType)) {
|
||||
if (devicetype && !validDeviceTypes.includes(devicetype)) {
|
||||
return next(errors.createError(400, `设备类型必须是以下之一: ${validDeviceTypes.join(', ')}`));
|
||||
}
|
||||
|
||||
@ -183,7 +183,7 @@ router.put(
|
||||
|
||||
// 查询该设备的所有配置,本地检查新密码是否与其他配置冲突
|
||||
const allAuths = await prisma.autoAuth.findMany({
|
||||
where: {deviceId: device.id},
|
||||
where: {deviceid: device.id},
|
||||
});
|
||||
|
||||
const conflictAuth = allAuths.find(auth =>
|
||||
@ -197,12 +197,12 @@ router.put(
|
||||
updateData.password = plainPassword;
|
||||
}
|
||||
|
||||
if (deviceType !== undefined) {
|
||||
updateData.deviceType = deviceType || null;
|
||||
if (devicetype !== undefined) {
|
||||
updateData.devicetype = devicetype || null;
|
||||
}
|
||||
|
||||
if (isReadOnly !== undefined) {
|
||||
updateData.isReadOnly = isReadOnly;
|
||||
if (isreadonly !== undefined) {
|
||||
updateData.isreadonly = isreadonly;
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
@ -216,9 +216,9 @@ router.put(
|
||||
config: {
|
||||
id: updatedAuth.id,
|
||||
password: updatedAuth.password, // 返回明文密码
|
||||
deviceType: updatedAuth.deviceType,
|
||||
isReadOnly: updatedAuth.isReadOnly,
|
||||
updatedAt: updatedAuth.updatedAt,
|
||||
devicetype: updatedAuth.devicetype,
|
||||
isreadonly: updatedAuth.isreadonly,
|
||||
updatedat: updatedAuth.updatedat,
|
||||
},
|
||||
});
|
||||
})
|
||||
@ -259,7 +259,7 @@ router.delete(
|
||||
}
|
||||
|
||||
// 确保配置属于当前设备
|
||||
if (autoAuth.deviceId !== device.id) {
|
||||
if (autoAuth.deviceid !== device.id) {
|
||||
return next(errors.createError(403, "无权操作此配置"));
|
||||
}
|
||||
|
||||
@ -334,7 +334,7 @@ router.put(
|
||||
uuid: updatedDevice.uuid,
|
||||
name: updatedDevice.name,
|
||||
namespace: updatedDevice.namespace,
|
||||
updatedAt: updatedDevice.updatedAt,
|
||||
updatedat: updatedDevice.updatedat,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {Router} from "express";
|
||||
import deviceCodeStore from "../utils/deviceCodeStore.js";
|
||||
import errors from "../utils/errors.js";
|
||||
import { prisma } from "../utils/prisma.js";
|
||||
import {prisma} from "../utils/prisma.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -62,7 +62,7 @@ router.post(
|
||||
}
|
||||
|
||||
// 验证token是否有效(检查数据库)
|
||||
const appInstall = await prisma.appInstall.findUnique({
|
||||
const appInstall = await prisma.appinstall.findUnique({
|
||||
where: {token},
|
||||
});
|
||||
|
||||
@ -193,7 +193,7 @@ router.get(
|
||||
exists: true,
|
||||
has_token: status.hasToken,
|
||||
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 {extractDeviceInfo} from "../middleware/uuidAuth.js";
|
||||
import {prisma} from "../utils/prisma.js";
|
||||
import errors from "../utils/errors.js";
|
||||
import {getOnlineDevices} from "../utils/socket.js";
|
||||
import {registeredDevicesTotal} from "../utils/metrics.js";
|
||||
import { prisma } from "../utils/prisma.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* 为新设备创建默认的自动登录配置
|
||||
* @param {number} deviceId - 设备ID
|
||||
* @param {number} deviceid - 设备ID
|
||||
*/
|
||||
async function createDefaultAutoAuth(deviceId) {
|
||||
async function createDefaultAutoAuth(deviceid) {
|
||||
try {
|
||||
// 创建默认的自动授权配置:不需要密码、类型是classroom(一体机)
|
||||
await prisma.autoAuth.create({
|
||||
data: {
|
||||
deviceId: deviceId,
|
||||
deviceid: deviceid,
|
||||
password: null, // 无密码
|
||||
deviceType: "classroom", // 一体机类型
|
||||
isReadOnly: false, // 非只读
|
||||
devicetype: "classroom", // 一体机类型
|
||||
isreadonly: false, // 非只读
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@ -90,7 +90,7 @@ router.post(
|
||||
uuid: device.uuid,
|
||||
name: device.name,
|
||||
namespace: device.namespace,
|
||||
createdAt: device.createdAt,
|
||||
createdat: device.createdat,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@ -117,7 +117,7 @@ router.get(
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
avatarUrl: true,
|
||||
avatarurl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -132,13 +132,13 @@ router.get(
|
||||
uuid: device.uuid,
|
||||
name: device.name,
|
||||
hasPassword: !!device.password,
|
||||
passwordHint: device.passwordHint,
|
||||
createdAt: device.createdAt,
|
||||
passwordhint: device.passwordhint,
|
||||
createdat: device.createdat,
|
||||
account: device.account ? {
|
||||
id: device.account.id,
|
||||
name: device.account.name,
|
||||
email: device.account.email,
|
||||
avatarUrl: device.account.avatarUrl,
|
||||
avatarurl: device.account.avatarurl,
|
||||
} : null,
|
||||
isBoundToAccount: !!device.account,
|
||||
namespace: device.namespace,
|
||||
@ -172,7 +172,7 @@ router.put(
|
||||
uuid: updatedDevice.uuid,
|
||||
name: updatedDevice.name,
|
||||
hasPassword: !!updatedDevice.password,
|
||||
passwordHint: updatedDevice.passwordHint,
|
||||
passwordhint: updatedDevice.passwordhint,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@ -28,11 +28,11 @@ router.get(
|
||||
"/_info",
|
||||
tokenReadLimiter,
|
||||
errors.catchAsync(async (req, res, next) => {
|
||||
const deviceId = res.locals.deviceId;
|
||||
const deviceid = res.locals.deviceid;
|
||||
|
||||
// 获取设备信息,包含关联的账号
|
||||
const device = await prisma.device.findUnique({
|
||||
where: { id: deviceId },
|
||||
where: { id: deviceid },
|
||||
include: {
|
||||
account: true,
|
||||
},
|
||||
@ -47,8 +47,8 @@ router.get(
|
||||
device: {
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
createdAt: device.createdAt,
|
||||
updatedAt: device.updatedAt,
|
||||
createdat: device.createdat,
|
||||
updatedat: device.updatedat,
|
||||
},
|
||||
};
|
||||
|
||||
@ -65,7 +65,7 @@ router.get(
|
||||
response.account = {
|
||||
id: device.account.id,
|
||||
name: device.account.name,
|
||||
avatarUrl: device.account.avatarUrl,
|
||||
avatarurl: device.account.avatarurl,
|
||||
};
|
||||
}
|
||||
|
||||
@ -82,10 +82,10 @@ router.get(
|
||||
tokenReadLimiter,
|
||||
errors.catchAsync(async (req, res, next) => {
|
||||
const token = res.locals.token;
|
||||
const deviceId = res.locals.deviceId;
|
||||
const deviceid = res.locals.deviceid;
|
||||
|
||||
// 查找当前 token 对应的应用安装记录
|
||||
const appInstall = await prisma.appInstall.findUnique({
|
||||
const appInstall = await prisma.appinstall.findUnique({
|
||||
where: { token },
|
||||
include: {
|
||||
device: {
|
||||
@ -107,11 +107,11 @@ router.get(
|
||||
success: true,
|
||||
token: appInstall.token,
|
||||
appId: appInstall.appId,
|
||||
deviceType: appInstall.deviceType,
|
||||
isReadOnly: appInstall.isReadOnly,
|
||||
devicetype: appInstall.devicetype,
|
||||
isreadonly: appInstall.isreadonly,
|
||||
note: appInstall.note,
|
||||
installedAt: appInstall.installedAt,
|
||||
updatedAt: appInstall.updatedAt,
|
||||
updatedat: appInstall.updatedat,
|
||||
device: {
|
||||
id: appInstall.device.id,
|
||||
uuid: appInstall.device.uuid,
|
||||
@ -130,7 +130,7 @@ router.get(
|
||||
"/_keys",
|
||||
tokenReadLimiter,
|
||||
errors.catchAsync(async (req, res) => {
|
||||
const deviceId = res.locals.deviceId;
|
||||
const deviceid = res.locals.deviceid;
|
||||
const { sortBy, sortDir, limit, skip } = req.query;
|
||||
|
||||
// 构建选项
|
||||
@ -141,7 +141,7 @@ router.get(
|
||||
skip: skip ? parseInt(skip) : 0,
|
||||
};
|
||||
|
||||
const keys = await kvStore.listKeysOnly(deviceId, options);
|
||||
const keys = await kvStore.listKeysOnly(deviceid, options);
|
||||
const totalRows = keys.length;
|
||||
|
||||
// 构建响应对象
|
||||
@ -181,7 +181,7 @@ router.get(
|
||||
"/",
|
||||
tokenReadLimiter,
|
||||
errors.catchAsync(async (req, res) => {
|
||||
const deviceId = res.locals.deviceId;
|
||||
const deviceid = res.locals.deviceid;
|
||||
const { sortBy, sortDir, limit, skip } = req.query;
|
||||
|
||||
// 构建选项
|
||||
@ -192,8 +192,8 @@ router.get(
|
||||
skip: skip ? parseInt(skip) : 0,
|
||||
};
|
||||
|
||||
const keys = await kvStore.list(deviceId, options);
|
||||
const totalRows = await kvStore.count(deviceId);
|
||||
const keys = await kvStore.list(deviceid, options);
|
||||
const totalRows = await kvStore.count(deviceid);
|
||||
|
||||
// 构建响应对象
|
||||
const response = {
|
||||
@ -227,10 +227,10 @@ router.get(
|
||||
"/:key",
|
||||
tokenReadLimiter,
|
||||
errors.catchAsync(async (req, res, next) => {
|
||||
const deviceId = res.locals.deviceId;
|
||||
const deviceid = res.locals.deviceid;
|
||||
const { key } = req.params;
|
||||
|
||||
const value = await kvStore.get(deviceId, key);
|
||||
const value = await kvStore.get(deviceid, key);
|
||||
|
||||
if (value === null) {
|
||||
return next(
|
||||
@ -250,10 +250,10 @@ router.get(
|
||||
"/:key/metadata",
|
||||
tokenReadLimiter,
|
||||
errors.catchAsync(async (req, res, next) => {
|
||||
const deviceId = res.locals.deviceId;
|
||||
const deviceid = res.locals.deviceid;
|
||||
const { key } = req.params;
|
||||
|
||||
const metadata = await kvStore.getMetadata(deviceId, key);
|
||||
const metadata = await kvStore.getMetadata(deviceid, key);
|
||||
if (!metadata) {
|
||||
return next(
|
||||
errors.createError(404, `未找到键名为 '${key}' 的记录`)
|
||||
@ -272,11 +272,11 @@ router.post(
|
||||
tokenBatchLimiter,
|
||||
errors.catchAsync(async (req, res, next) => {
|
||||
// 检查token是否为只读
|
||||
if (res.locals.appInstall?.isReadOnly) {
|
||||
if (res.locals.appInstall?.isreadonly) {
|
||||
return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
|
||||
}
|
||||
|
||||
const deviceId = res.locals.deviceId;
|
||||
const deviceid = res.locals.deviceid;
|
||||
const data = req.body;
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
@ -289,7 +289,7 @@ router.post(
|
||||
}
|
||||
|
||||
// 获取客户端IP
|
||||
const creatorIp =
|
||||
const creatorip =
|
||||
req.headers["x-forwarded-for"] ||
|
||||
req.connection.remoteAddress ||
|
||||
req.socket.remoteAddress ||
|
||||
@ -297,13 +297,13 @@ router.post(
|
||||
"";
|
||||
|
||||
// 使用优化的批量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({
|
||||
code: 200,
|
||||
message: "批量导入成功",
|
||||
data: {
|
||||
deviceId,
|
||||
deviceid,
|
||||
summary: {
|
||||
total: Object.keys(data).length,
|
||||
successful: results.length,
|
||||
@ -328,11 +328,11 @@ router.post(
|
||||
tokenWriteLimiter,
|
||||
errors.catchAsync(async (req, res, next) => {
|
||||
// 检查token是否为只读
|
||||
if (res.locals.appInstall?.isReadOnly) {
|
||||
if (res.locals.appInstall?.isreadonly) {
|
||||
return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
|
||||
}
|
||||
|
||||
const deviceId = res.locals.deviceId;
|
||||
const deviceid = res.locals.deviceid;
|
||||
const { key } = req.params;
|
||||
let value = req.body;
|
||||
|
||||
@ -351,14 +351,14 @@ router.post(
|
||||
}
|
||||
|
||||
// 获取客户端IP
|
||||
const creatorIp =
|
||||
const creatorip =
|
||||
req.headers["x-forwarded-for"] ||
|
||||
req.connection.remoteAddress ||
|
||||
req.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;
|
||||
@ -366,16 +366,16 @@ router.post(
|
||||
broadcastKeyChanged(uuid, {
|
||||
key: result.key,
|
||||
action: "upsert",
|
||||
created: result.createdAt.getTime() === result.updatedAt.getTime(),
|
||||
updatedAt: result.updatedAt,
|
||||
created: result.createdat.getTime() === result.updatedat.getTime(),
|
||||
updatedat: result.updatedat,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
deviceId: result.deviceId,
|
||||
deviceid: result.deviceid,
|
||||
key: result.key,
|
||||
created: result.createdAt.getTime() === result.updatedAt.getTime(),
|
||||
updatedAt: result.updatedAt,
|
||||
created: result.createdat.getTime() === result.updatedat.getTime(),
|
||||
updatedat: result.updatedat,
|
||||
});
|
||||
})
|
||||
);
|
||||
@ -389,14 +389,14 @@ router.delete(
|
||||
tokenDeleteLimiter,
|
||||
errors.catchAsync(async (req, res, next) => {
|
||||
// 检查token是否为只读
|
||||
if (res.locals.appInstall?.isReadOnly) {
|
||||
if (res.locals.appInstall?.isreadonly) {
|
||||
return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
|
||||
}
|
||||
|
||||
const deviceId = res.locals.deviceId;
|
||||
const deviceid = res.locals.deviceid;
|
||||
const { key } = req.params;
|
||||
|
||||
const result = await kvStore.delete(deviceId, key);
|
||||
const result = await kvStore.delete(deviceid, key);
|
||||
|
||||
if (!result) {
|
||||
return next(
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
class DeviceCodeStore {
|
||||
constructor() {
|
||||
// 存储结构: { deviceCode: { token: string, expiresAt: number, createdAt: number } }
|
||||
// 存储结构: { deviceCode: { token: string, expiresAt: number, createdat: number } }
|
||||
this.store = new Map();
|
||||
|
||||
// 默认过期时间: 15分钟
|
||||
@ -44,7 +44,7 @@ class DeviceCodeStore {
|
||||
this.store.set(deviceCode, {
|
||||
token: null,
|
||||
expiresAt: now + this.expirationTime,
|
||||
createdAt: now,
|
||||
createdat: now,
|
||||
});
|
||||
|
||||
return deviceCode;
|
||||
@ -143,7 +143,7 @@ class DeviceCodeStore {
|
||||
return {
|
||||
hasToken: !!entry.token,
|
||||
expiresAt: entry.expiresAt,
|
||||
createdAt: entry.createdAt,
|
||||
createdat: entry.createdat,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ export function generateAccountToken(account) {
|
||||
provider: account.provider,
|
||||
email: account.email,
|
||||
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 { prisma } from "./prisma.js";
|
||||
|
||||
class KVStore {
|
||||
/**
|
||||
* 通过设备ID和键名获取值
|
||||
* @param {number} deviceId - 设备ID
|
||||
* @param {number} deviceid - 设备ID
|
||||
* @param {string} key - 键名
|
||||
* @returns {object|null} 键对应的值或null
|
||||
*/
|
||||
async get(deviceId, key) {
|
||||
const item = await prisma.kVStore.findUnique({
|
||||
async get(deviceid, key) {
|
||||
const item = await prisma.kvstore.findUnique({
|
||||
where: {
|
||||
deviceId_key: {
|
||||
deviceId: deviceId,
|
||||
deviceid_key: {
|
||||
deviceid: deviceid,
|
||||
key: key,
|
||||
},
|
||||
},
|
||||
@ -22,24 +22,24 @@ class KVStore {
|
||||
|
||||
/**
|
||||
* 获取键的完整信息(包括元数据)
|
||||
* @param {number} deviceId - 设备ID
|
||||
* @param {number} deviceid - 设备ID
|
||||
* @param {string} key - 键名
|
||||
* @returns {object|null} 键的完整信息或null
|
||||
*/
|
||||
async getMetadata(deviceId, key) {
|
||||
const item = await prisma.kVStore.findUnique({
|
||||
async getMetadata(deviceid, key) {
|
||||
const item = await prisma.kvstore.findUnique({
|
||||
where: {
|
||||
deviceId_key: {
|
||||
deviceId: deviceId,
|
||||
deviceid_key: {
|
||||
deviceid: deviceid,
|
||||
key: key,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
key: true,
|
||||
deviceId: true,
|
||||
creatorIp: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
deviceid: true,
|
||||
creatorip: true,
|
||||
createdat: true,
|
||||
updatedat: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -47,67 +47,67 @@ class KVStore {
|
||||
|
||||
// 转换为更友好的格式
|
||||
return {
|
||||
deviceId: item.deviceId,
|
||||
deviceid: item.deviceid,
|
||||
key: item.key,
|
||||
metadata: {
|
||||
creatorIp: item.creatorIp,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
creatorip: item.creatorip,
|
||||
createdat: item.createdat,
|
||||
updatedat: item.updatedat,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定设备下创建或更新键值
|
||||
* @param {number} deviceId - 设备ID
|
||||
* @param {number} deviceid - 设备ID
|
||||
* @param {string} key - 键名
|
||||
* @param {object} value - 键值
|
||||
* @param {string} creatorIp - 创建者IP,可选
|
||||
* @param {string} creatorip - 创建者IP,可选
|
||||
* @returns {object} 创建或更新的记录
|
||||
*/
|
||||
async upsert(deviceId, key, value, creatorIp = "") {
|
||||
const item = await prisma.kVStore.upsert({
|
||||
async upsert(deviceid, key, value, creatorip = "") {
|
||||
const item = await prisma.kvstore.upsert({
|
||||
where: {
|
||||
deviceId_key: {
|
||||
deviceId: deviceId,
|
||||
deviceid_key: {
|
||||
deviceid: deviceid,
|
||||
key: key,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
value,
|
||||
...(creatorIp && {creatorIp}),
|
||||
...(creatorip && {creatorip}),
|
||||
},
|
||||
create: {
|
||||
deviceId: deviceId,
|
||||
deviceid: deviceid,
|
||||
key: key,
|
||||
value,
|
||||
creatorIp,
|
||||
creatorip: creatorip,
|
||||
},
|
||||
});
|
||||
|
||||
// 更新键总数指标
|
||||
const totalKeys = await prisma.kVStore.count();
|
||||
const totalKeys = await prisma.kvstore.count();
|
||||
keysTotal.set(totalKeys);
|
||||
|
||||
// 返回带有设备ID和原始键的结果
|
||||
return {
|
||||
deviceId,
|
||||
deviceid,
|
||||
key,
|
||||
value: item.value,
|
||||
creatorIp: item.creatorIp,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
creatorip: item.creatorip,
|
||||
createdat: item.createdat,
|
||||
updatedat: item.updatedat,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建或更新键值对(优化性能)
|
||||
* @param {number} deviceId - 设备ID
|
||||
* @param {number} deviceid - 设备ID
|
||||
* @param {object} data - 键值对数据 {key1: value1, key2: value2, ...}
|
||||
* @param {string} creatorIp - 创建者IP,可选
|
||||
* @param {string} creatorip - 创建者IP,可选
|
||||
* @returns {object} {results: Array, errors: Array}
|
||||
*/
|
||||
async batchUpsert(deviceId, data, creatorIp = "") {
|
||||
async batchUpsert(deviceid, data, creatorip = "") {
|
||||
const results = [];
|
||||
const errors = [];
|
||||
|
||||
@ -115,30 +115,30 @@ class KVStore {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
try {
|
||||
const item = await tx.kVStore.upsert({
|
||||
const item = await tx.kvstore.upsert({
|
||||
where: {
|
||||
deviceId_key: {
|
||||
deviceId: deviceId,
|
||||
deviceid_key: {
|
||||
deviceid: deviceid,
|
||||
key: key,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
value,
|
||||
...(creatorIp && {creatorIp}),
|
||||
...(creatorip && {creatorip: creatorip}),
|
||||
},
|
||||
create: {
|
||||
deviceId: deviceId,
|
||||
deviceid: deviceid,
|
||||
key: key,
|
||||
value,
|
||||
creatorIp,
|
||||
creatorip: creatorip,
|
||||
},
|
||||
});
|
||||
|
||||
results.push({
|
||||
key: item.key,
|
||||
created: item.createdAt.getTime() === item.updatedAt.getTime(),
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
created: item.createdat.getTime() === item.updatedat.getTime(),
|
||||
createdat: item.createdat,
|
||||
updatedat: item.updatedat,
|
||||
});
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
@ -150,7 +150,7 @@ class KVStore {
|
||||
});
|
||||
|
||||
// 在事务完成后,一次性更新指标
|
||||
const totalKeys = await prisma.kVStore.count();
|
||||
const totalKeys = await prisma.kvstore.count();
|
||||
keysTotal.set(totalKeys);
|
||||
|
||||
return { results, errors };
|
||||
@ -158,26 +158,26 @@ class KVStore {
|
||||
|
||||
/**
|
||||
* 通过设备ID和键名删除
|
||||
* @param {number} deviceId - 设备ID
|
||||
* @param {number} deviceid - 设备ID
|
||||
* @param {string} key - 键名
|
||||
* @returns {object|null} 删除的记录或null
|
||||
*/
|
||||
async delete(deviceId, key) {
|
||||
async delete(deviceid, key) {
|
||||
try {
|
||||
const item = await prisma.kVStore.delete({
|
||||
const item = await prisma.kvstore.delete({
|
||||
where: {
|
||||
deviceId_key: {
|
||||
deviceId: deviceId,
|
||||
deviceid_key: {
|
||||
deviceid: deviceid,
|
||||
key: key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 更新键总数指标
|
||||
const totalKeys = await prisma.kVStore.count();
|
||||
const totalKeys = await prisma.kvstore.count();
|
||||
keysTotal.set(totalKeys);
|
||||
|
||||
return item ? {...item, deviceId, key} : null;
|
||||
return item ? {...item, deviceid, key} : null;
|
||||
} catch (error) {
|
||||
// 忽略记录不存在的错误
|
||||
if (error.code === "P2025") {
|
||||
@ -189,11 +189,11 @@ class KVStore {
|
||||
|
||||
/**
|
||||
* 列出指定设备下的所有键名及其元数据
|
||||
* @param {number} deviceId - 设备ID
|
||||
* @param {number} deviceid - 设备ID
|
||||
* @param {object} options - 选项参数
|
||||
* @returns {Array} 键名和元数据数组
|
||||
*/
|
||||
async list(deviceId, options = {}) {
|
||||
async list(deviceid, options = {}) {
|
||||
const {sortBy = "key", sortDir = "asc", limit = 100, skip = 0} = options;
|
||||
|
||||
// 构建排序条件
|
||||
@ -201,16 +201,16 @@ class KVStore {
|
||||
orderBy[sortBy] = sortDir.toLowerCase();
|
||||
|
||||
// 查询设备的所有键
|
||||
const items = await prisma.kVStore.findMany({
|
||||
const items = await prisma.kvstore.findMany({
|
||||
where: {
|
||||
deviceId: deviceId,
|
||||
deviceid: deviceid,
|
||||
},
|
||||
select: {
|
||||
deviceId: true,
|
||||
deviceid: true,
|
||||
key: true,
|
||||
creatorIp: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
creatorip: true,
|
||||
createdat: true,
|
||||
updatedat: true,
|
||||
value: false,
|
||||
},
|
||||
orderBy,
|
||||
@ -220,23 +220,23 @@ class KVStore {
|
||||
|
||||
// 处理结果
|
||||
return items.map((item) => ({
|
||||
deviceId: item.deviceId,
|
||||
deviceid: item.deviceid,
|
||||
key: item.key,
|
||||
metadata: {
|
||||
creatorIp: item.creatorIp,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
creatorip: item.creatorip,
|
||||
createdat: item.createdat,
|
||||
updatedat: item.updatedat,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定设备下的键名列表(不包括内容)
|
||||
* @param {number} deviceId - 设备ID
|
||||
* @param {number} deviceid - 设备ID
|
||||
* @param {object} options - 查询选项
|
||||
* @returns {Array} 键名列表
|
||||
*/
|
||||
async listKeysOnly(deviceId, options = {}) {
|
||||
async listKeysOnly(deviceid, options = {}) {
|
||||
const {sortBy = "key", sortDir = "asc", limit = 100, skip = 0} = options;
|
||||
|
||||
// 构建排序条件
|
||||
@ -244,9 +244,9 @@ class KVStore {
|
||||
orderBy[sortBy] = sortDir.toLowerCase();
|
||||
|
||||
// 查询设备的所有键,只选择键名
|
||||
const items = await prisma.kVStore.findMany({
|
||||
const items = await prisma.kvstore.findMany({
|
||||
where: {
|
||||
deviceId: deviceId,
|
||||
deviceid: deviceid,
|
||||
},
|
||||
select: {
|
||||
key: true,
|
||||
@ -262,13 +262,13 @@ class KVStore {
|
||||
|
||||
/**
|
||||
* 统计指定设备下的键值对数量
|
||||
* @param {number} deviceId - 设备ID
|
||||
* @param {number} deviceid - 设备ID
|
||||
* @returns {number} 键值对数量
|
||||
*/
|
||||
async count(deviceId) {
|
||||
const count = await prisma.kVStore.count({
|
||||
async count(deviceid) {
|
||||
const count = await prisma.kvstore.count({
|
||||
where: {
|
||||
deviceId: deviceId,
|
||||
deviceid: deviceid,
|
||||
},
|
||||
});
|
||||
return count;
|
||||
@ -276,32 +276,32 @@ class KVStore {
|
||||
|
||||
/**
|
||||
* 获取指定设备的统计信息
|
||||
* @param {number} deviceId - 设备ID
|
||||
* @param {number} deviceid - 设备ID
|
||||
* @returns {object} 统计信息
|
||||
*/
|
||||
async getStats(deviceId) {
|
||||
async getStats(deviceid) {
|
||||
const [totalKeys, oldestKey, newestKey] = await Promise.all([
|
||||
prisma.kVStore.count({
|
||||
where: { deviceId },
|
||||
prisma.kvstore.count({
|
||||
where: { deviceid },
|
||||
}),
|
||||
prisma.kVStore.findFirst({
|
||||
where: { deviceId },
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: { createdAt: true, key: true },
|
||||
prisma.kvstore.findFirst({
|
||||
where: { deviceid },
|
||||
orderBy: { createdat: "asc" },
|
||||
select: { createdat: true, key: true },
|
||||
}),
|
||||
prisma.kVStore.findFirst({
|
||||
where: { deviceId },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
select: { updatedAt: true, key: true },
|
||||
prisma.kvstore.findFirst({
|
||||
where: { deviceid },
|
||||
orderBy: { updatedat: "desc" },
|
||||
select: { updatedat: true, key: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
totalKeys,
|
||||
oldestKey: oldestKey?.key,
|
||||
oldestCreatedAt: oldestKey?.createdAt,
|
||||
oldestCreatedAt: oldestKey?.createdat,
|
||||
newestKey: newestKey?.key,
|
||||
newestUpdatedAt: newestKey?.updatedAt,
|
||||
newestUpdatedAt: newestKey?.updatedat,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,8 +33,9 @@ export async function initializeMetrics() {
|
||||
registeredDevicesTotal.set(deviceCount);
|
||||
|
||||
// 获取已创建键总数
|
||||
const keyCount = await prisma.kVStore.count();
|
||||
const keyCount = await prisma.kvstore.count();
|
||||
keysTotal.set(keyCount);
|
||||
|
||||
console.log('Prometheus metrics initialized - Devices:', deviceCount, 'Keys:', keyCount);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize metrics:', error);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {prisma} from "./prisma.js";
|
||||
import kvStore from "./kvStore.js";
|
||||
import { prisma } from "./prisma.js";
|
||||
|
||||
// 系统保留UUID用于存储站点信息
|
||||
const SYSTEM_DEVICE_UUID = "00000000-0000-4000-8000-000000000000";
|
||||
@ -46,8 +46,8 @@ async function getSystemDeviceId() {
|
||||
*/
|
||||
export const initReadme = async () => {
|
||||
try {
|
||||
const deviceId = await getSystemDeviceId();
|
||||
const storedValue = await kvStore.get(deviceId, "info");
|
||||
const deviceid = await getSystemDeviceId();
|
||||
const storedValue = await kvStore.get(deviceid, "info");
|
||||
|
||||
// 合并默认值与存储值,确保结构完整
|
||||
readmeValue = {
|
||||
@ -82,8 +82,8 @@ export const getReadmeValue = () => {
|
||||
*/
|
||||
export const updateReadmeValue = async (newValue) => {
|
||||
try {
|
||||
const deviceId = await getSystemDeviceId();
|
||||
await kvStore.upsert(deviceId, "info", newValue);
|
||||
const deviceid = await getSystemDeviceId();
|
||||
await kvStore.upsert(deviceid, "info", newValue);
|
||||
readmeValue = {
|
||||
...defaultReadme,
|
||||
...newValue,
|
||||
|
||||
@ -13,10 +13,10 @@
|
||||
*/
|
||||
|
||||
import { Server } from "socket.io";
|
||||
import { prisma } from "./prisma.js";
|
||||
import { onlineDevicesGauge } from "./metrics.js";
|
||||
import DeviceDetector from "node-device-detector";
|
||||
import ClientHints from "node-device-detector/client-hints.js";
|
||||
import { prisma } from "./prisma.js";
|
||||
|
||||
// Socket.IO 单例实例
|
||||
let io = null;
|
||||
@ -33,7 +33,7 @@ const clientHints = new ClientHints();
|
||||
const onlineMap = new Map();
|
||||
// 在线 token 映射:token -> Set<socketId> (用于指标统计)
|
||||
const onlineTokens = new Map();
|
||||
// 令牌信息缓存:token -> {appId, isReadOnly, deviceType, note, deviceUuid, deviceName}
|
||||
// 令牌信息缓存:token -> {appId, isreadonly, devicetype, note, deviceUuid, deviceName}
|
||||
const tokenInfoCache = new Map();
|
||||
// 事件历史记录:每个设备最多保存1000条事件记录
|
||||
const eventHistory = new Map(); // uuid -> Array<EventRecord>
|
||||
@ -132,7 +132,7 @@ export function initSocket(server) {
|
||||
try {
|
||||
const token = payload?.token || payload?.apptoken;
|
||||
if (typeof token !== "string" || token.length === 0) return;
|
||||
const appInstall = await prisma.appInstall.findUnique({
|
||||
const appInstall = await prisma.appinstall.findUnique({
|
||||
where: { token },
|
||||
include: { device: { select: { uuid: true } } },
|
||||
});
|
||||
@ -175,9 +175,9 @@ export function initSocket(server) {
|
||||
devices: historyData,
|
||||
timestamp: new Date().toISOString(),
|
||||
requestedBy: {
|
||||
deviceType: socket.data.tokenInfo?.deviceType,
|
||||
devicetype: socket.data.tokenInfo?.devicetype,
|
||||
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;
|
||||
if (tokenInfo?.isReadOnly) {
|
||||
if (tokenInfo?.isreadonly) {
|
||||
socket.emit("event-error", { reason: "readonly_token_cannot_send_events" });
|
||||
return;
|
||||
}
|
||||
@ -243,9 +243,9 @@ export function initSocket(server) {
|
||||
senderId: socket.id,
|
||||
senderInfo: {
|
||||
appId: tokenInfo?.appId,
|
||||
deviceType: tokenInfo?.deviceType,
|
||||
devicetype: tokenInfo?.devicetype,
|
||||
deviceName: tokenInfo?.note,
|
||||
isReadOnly: tokenInfo?.isReadOnly || false,
|
||||
isreadonly: tokenInfo?.isreadonly || false,
|
||||
note: tokenInfo?.note
|
||||
}
|
||||
};
|
||||
@ -385,7 +385,7 @@ function removeTokenConnection(token, socketId) {
|
||||
/**
|
||||
* 广播某设备下 KV 键已变更
|
||||
* @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) {
|
||||
if (!io || !uuid) return;
|
||||
@ -400,9 +400,9 @@ export function broadcastKeyChanged(uuid, payload) {
|
||||
senderId: "realtime",
|
||||
senderInfo: {
|
||||
appId: "5c2a54d553951a37b47066ead68c8642",
|
||||
deviceType: "server",
|
||||
devicetype: "server",
|
||||
deviceName: "realtime",
|
||||
isReadOnly: false,
|
||||
isreadonly: false,
|
||||
note: "Database realtime sync"
|
||||
}
|
||||
};
|
||||
@ -443,9 +443,9 @@ export function broadcastDeviceEvent(uuid, type, content = null, senderId = "sys
|
||||
senderId,
|
||||
senderInfo: {
|
||||
appId: "system",
|
||||
deviceType: "system",
|
||||
devicetype: "system",
|
||||
deviceName: "System",
|
||||
isReadOnly: false,
|
||||
isreadonly: false,
|
||||
note: "System broadcast"
|
||||
}
|
||||
};
|
||||
@ -549,7 +549,7 @@ export default {
|
||||
*/
|
||||
async function joinByToken(socket, token) {
|
||||
try {
|
||||
const appInstall = await prisma.appInstall.findUnique({
|
||||
const appInstall = await prisma.appinstall.findUnique({
|
||||
where: { token },
|
||||
include: {
|
||||
device: {
|
||||
@ -576,8 +576,8 @@ async function joinByToken(socket, token) {
|
||||
// 缓存令牌信息,使用拼接后的设备名称
|
||||
const tokenInfo = {
|
||||
appId: appInstall.appId,
|
||||
isReadOnly: appInstall.isReadOnly,
|
||||
deviceType: appInstall.deviceType,
|
||||
isreadonly: appInstall.isreadonly,
|
||||
devicetype: appInstall.devicetype,
|
||||
note: appInstall.note,
|
||||
deviceUuid: uuid,
|
||||
deviceName: finalDeviceName, // 使用拼接后的设备名称
|
||||
@ -600,8 +600,8 @@ async function joinByToken(socket, token) {
|
||||
uuid,
|
||||
token,
|
||||
tokenInfo: {
|
||||
isReadOnly: tokenInfo.isReadOnly,
|
||||
deviceType: tokenInfo.deviceType,
|
||||
isreadonly: tokenInfo.isreadonly,
|
||||
devicetype: tokenInfo.devicetype,
|
||||
deviceName: tokenInfo.deviceName,
|
||||
userAgent: userAgent
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import crypto from 'crypto';
|
||||
import { prisma } from './prisma.js';
|
||||
import {prisma} from './prisma.js';
|
||||
|
||||
// Token 配置
|
||||
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,
|
||||
email: account.email,
|
||||
name: account.name,
|
||||
avatarUrl: account.avatarUrl,
|
||||
tokenVersion: account.tokenVersion || 1,
|
||||
avatarurl: account.avatarurl,
|
||||
tokenversion: account.tokenversion || 1,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, signKey, {
|
||||
@ -71,7 +71,7 @@ export function generateRefreshToken(account) {
|
||||
const payload = {
|
||||
type: 'refresh',
|
||||
accountId: account.id,
|
||||
tokenVersion: account.tokenVersion || 1,
|
||||
tokenversion: account.tokenversion || 1,
|
||||
// 添加随机字符串增加安全性
|
||||
jti: crypto.randomBytes(16).toString('hex'),
|
||||
};
|
||||
@ -134,39 +134,39 @@ export function verifyRefreshToken(token) {
|
||||
* 生成令牌对(访问令牌 + 刷新令牌)
|
||||
*/
|
||||
export async function generateTokenPair(account) {
|
||||
const accessToken = generateAccessToken(account);
|
||||
const refreshToken = generateRefreshToken(account);
|
||||
const accesstoken = generateAccessToken(account);
|
||||
const refreshtoken = generateRefreshToken(account);
|
||||
|
||||
// 计算刷新令牌过期时间
|
||||
const refreshTokenExpiry = new Date();
|
||||
const refreshtokenExpiry = new Date();
|
||||
const expiresInMs = parseExpirationToMs(REFRESH_TOKEN_EXPIRES_IN);
|
||||
refreshTokenExpiry.setTime(refreshTokenExpiry.getTime() + expiresInMs);
|
||||
refreshtokenExpiry.setTime(refreshtokenExpiry.getTime() + expiresInMs);
|
||||
|
||||
// 更新数据库中的刷新令牌
|
||||
await prisma.account.update({
|
||||
where: {id: account.id},
|
||||
data: {
|
||||
refreshToken,
|
||||
refreshTokenExpiry,
|
||||
updatedAt: new Date(),
|
||||
refreshtoken,
|
||||
refreshtokenExpiry,
|
||||
updatedat: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
accessTokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
|
||||
refreshTokenExpiresIn: REFRESH_TOKEN_EXPIRES_IN,
|
||||
accesstoken,
|
||||
refreshtoken,
|
||||
accesstokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
|
||||
refreshtokenExpiresIn: REFRESH_TOKEN_EXPIRES_IN,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*/
|
||||
export async function refreshAccessToken(refreshToken) {
|
||||
export async function refreshAccessToken(refreshtoken) {
|
||||
try {
|
||||
// 验证刷新令牌
|
||||
const decoded = verifyRefreshToken(refreshToken);
|
||||
const decoded = verifyRefreshToken(refreshtoken);
|
||||
|
||||
// 从数据库获取账户信息
|
||||
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');
|
||||
}
|
||||
|
||||
// 验证刷新令牌是否过期
|
||||
if (account.refreshTokenExpiry && account.refreshTokenExpiry < new Date()) {
|
||||
if (account.refreshtokenExpiry && account.refreshtokenExpiry < new Date()) {
|
||||
throw new Error('Refresh token expired');
|
||||
}
|
||||
|
||||
// 验证令牌版本
|
||||
if (account.tokenVersion !== decoded.tokenVersion) {
|
||||
if (account.tokenversion !== decoded.tokenversion) {
|
||||
throw new Error('Token version mismatch');
|
||||
}
|
||||
|
||||
@ -196,14 +196,14 @@ export async function refreshAccessToken(refreshToken) {
|
||||
const newAccessToken = generateAccessToken(account);
|
||||
|
||||
return {
|
||||
accessToken: newAccessToken,
|
||||
accessTokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
|
||||
accesstoken: newAccessToken,
|
||||
accesstokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
|
||||
account: {
|
||||
id: account.id,
|
||||
provider: account.provider,
|
||||
email: account.email,
|
||||
name: account.name,
|
||||
avatarUrl: account.avatarUrl,
|
||||
avatarurl: account.avatarurl,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
@ -218,10 +218,10 @@ export async function revokeAllTokens(accountId) {
|
||||
await prisma.account.update({
|
||||
where: {id: accountId},
|
||||
data: {
|
||||
tokenVersion: {increment: 1},
|
||||
refreshToken: null,
|
||||
refreshTokenExpiry: null,
|
||||
updatedAt: new Date(),
|
||||
tokenversion: {increment: 1},
|
||||
refreshtoken: null,
|
||||
refreshtokenExpiry: null,
|
||||
updatedat: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -233,9 +233,9 @@ export async function revokeRefreshToken(accountId) {
|
||||
await prisma.account.update({
|
||||
where: {id: accountId},
|
||||
data: {
|
||||
refreshToken: null,
|
||||
refreshTokenExpiry: null,
|
||||
updatedAt: new Date(),
|
||||
refreshtoken: null,
|
||||
refreshtokenExpiry: null,
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user