1
1
mirror of https://github.com/ZeroCatDev/ClassworksKV.git synced 2026-02-04 07:44:40 +00:00

Compare commits

..

9 Commits

Author SHA1 Message Date
Sunwuyuan
88d5062638
1.3.15 2026-02-01 21:15:54 +08:00
Sunwuyuan
2f7f78f657
fix: set default values for id fields in Account, AppInstall, AutoAuth models and create migration for Device sequence 2026-02-01 21:15:49 +08:00
Sunwuyuan
80e7214860
1.3.14 2026-02-01 20:51:15 +08:00
Sunwuyuan
e7f155b021
1.3.13 2026-02-01 20:08:29 +08:00
Sunwuyuan
8c8ebae6b7
迁移到postgresql(叹气) 2026-02-01 20:08:13 +08:00
Sunwuyuan
6e8ba54fd6
1.3.12 2026-02-01 19:10:07 +08:00
Sunwuyuan
dc5de6e63a
fix: standardize autoAuth field names to autoauth in apps and auto-auth routes 2026-02-01 19:09:59 +08:00
Sunwuyuan
c063dcf5b6
1.3.11 2026-02-01 19:00:19 +08:00
Sunwuyuan
29030f90bd
fix: standardize device-related field names and timestamps across routes and utils
- Updated field names from camelCase to lowercase (e.g., deviceId to deviceid, createdAt to createdat) in auto-auth, device-auth, device, kv-token, and other related files.
- Adjusted database migration scripts to set default timestamps with timezone for createdat and updatedat fields in relevant tables.
- Ensured consistency in handling device-related data across the application.
2026-02-01 18:59:58 +08:00
50 changed files with 12321 additions and 147 deletions

4
.gitignore vendored
View File

@ -145,7 +145,3 @@ dist
prisma/database/data prisma/database/data
data/ data/
/generated/prisma
/generated/prisma

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/FixClassworksKV.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/FixClassworksKV.iml" filepath="$PROJECT_DIR$/.idea/FixClassworksKV.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

276
API_AUTOAUTH.md Normal file
View File

@ -0,0 +1,276 @@
# AutoAuth 和新增 Apps API 文档
## 概述
本文档描述了自动授权 (AutoAuth) 相关的 API 接口以及新增的应用管理接口。
---
## Apps API 新增接口
### 1. 通过 namespace 和密码获取 token
**端点**: `POST /apps/auth/token`
**描述**: 通过设备的 namespace 和密码进行自动授权,创建新的 AppInstall 并返回 token。
**请求体**:
```json
{
"namespace": "string (必填)",
"password": "string (可选,根据自动授权配置)",
"appId": "string (必填)"
}
```
**成功响应** (201 Created):
```json
{
"success": true,
"token": "string",
"deviceType": "string | null",
"isReadOnly": boolean,
"installedAt": "datetime"
}
```
**错误响应**:
- `400 Bad Request`: 缺少必填字段
- `404 Not Found`: 设备不存在或 namespace 不正确
- `401 Unauthorized`: 密码不正确或需要提供密码
**说明**:
- 该接口会查找匹配的 AutoAuth 配置
- 如果提供了密码,会验证密码是否匹配任何自动授权配置
- 如果没有提供密码,会查找无密码的自动授权配置
- 根据匹配的 AutoAuth 配置设置 `deviceType``isReadOnly` 属性
---
### 2. 设置学生名称
**端点**: `POST /apps/tokens/:token/set-student-name`
**描述**: 为学生类型的 token 设置名称(更新 note 字段)。
**URL 参数**:
- `token`: AppInstall 的 token
**请求体**:
```json
{
"name": "string (必填)"
}
```
**成功响应** (200 OK):
```json
{
"success": true,
"token": "string",
"name": "string",
"updatedAt": "datetime"
}
```
**错误响应**:
- `400 Bad Request`: 缺少名称或名称不在学生列表中
- `403 Forbidden`: token 类型不是 student
- `404 Not Found`: token 不存在或设备未设置学生列表
**说明**:
- 只有 `deviceType``student` 的 token 才能使用此接口
- 会验证提供的名称是否存在于设备的 `classworks-list-main` 键值中
- 学生列表格式: `[{"id": 1, "name": "学生1"}, {"id": 2, "name": "学生2"}]`
---
## AutoAuth 管理 API
> 🔐 **所有 AutoAuth 管理接口都需要 JWT Account Token 认证**
>
> **重要**: 只有已绑定账户的设备才能使用这些接口。未绑定账户的设备无法管理 AutoAuth 配置。
>
> 通过 HTTP Headers 提供:
> - `Authorization`: `Bearer {jwt_token}` - 账户的 JWT Token
### 1. 获取设备的自动授权配置列表
**端点**: `GET /auto-auth/devices/:uuid/auth-configs`
**认证**: 需要 JWT Token (账户必须是设备的拥有者)
**URL 参数**:
- `uuid`: 设备的 UUID
**成功响应** (200 OK):
```json
{
"success": true,
"configs": [
{
"id": "string",
"hasPassword": boolean,
"deviceType": "string | null",
"isReadOnly": boolean,
"createdAt": "datetime",
"updatedAt": "datetime"
}
]
}
```
**说明**:
- 返回的配置不包含实际的密码哈希值,只显示是否有密码
---
### 2. 创建自动授权配置
**端点**: `POST /auto-auth/devices/:uuid/auth-configs`
**认证**: 需要 JWT Token (账户必须是设备的拥有者)
**URL 参数**:
- `uuid`: 设备的 UUID
**请求体**:
```json
{
"password": "string (可选)",
"deviceType": "string (可选: teacher|student|classroom|parent)",
"isReadOnly": boolean (可选,默认 false)
}
```
**成功响应** (201 Created):
```json
{
"success": true,
"config": {
"id": "string",
"hasPassword": boolean,
"deviceType": "string | null",
"isReadOnly": boolean,
"createdAt": "datetime"
}
}
```
**错误响应**:
- `400 Bad Request`: 设备类型无效或密码配置已存在
**说明**:
- 同一设备的密码必须唯一(包括空密码)
- `deviceType` 必须是 `teacher``student``classroom``parent` 之一,或为空
---
### 3. 更新自动授权配置
**端点**: `PUT /auto-auth/devices/:uuid/auth-configs/:configId`
**认证**: 需要 JWT Token (账户必须是设备的拥有者)
**URL 参数**:
- `uuid`: 设备的 UUID
- `configId`: 自动授权配置的 ID
**请求体**:
```json
{
"password": "string (可选)",
"deviceType": "string (可选: teacher|student|classroom|parent)",
"isReadOnly": boolean (可选)
}
```
**成功响应** (200 OK):
```json
{
"success": true,
"config": {
"id": "string",
"hasPassword": boolean,
"deviceType": "string | null",
"isReadOnly": boolean,
"updatedAt": "datetime"
}
}
```
**错误响应**:
- `400 Bad Request`: 设备类型无效或新密码与其他配置冲突
- `403 Forbidden`: 无权操作此配置
- `404 Not Found`: 配置不存在
**说明**:
- 只能更新属于当前设备的配置
- 更新密码时会检查是否与该设备的其他配置冲突
---
### 4. 删除自动授权配置
**端点**: `DELETE /auto-auth/devices/:uuid/auth-configs/:configId`
**认证**: 需要 JWT Token (账户必须是设备的拥有者)
**URL 参数**:
- `uuid`: 设备的 UUID
- `configId`: 自动授权配置的 ID
**成功响应** (204 No Content):
- 无响应体
**错误响应**:
- `403 Forbidden`: 无权操作此配置
- `404 Not Found`: 配置不存在
**说明**:
- 只能删除属于当前设备的配置
---
## 设备类型 (deviceType)
可选的设备类型值:
- `teacher`: 教师
- `student`: 学生
- `classroom`: 班级一体机
- `parent`: 家长
- `null`: 未指定类型
---
## 使用流程示例
### 场景 1: 学生使用 namespace 登录
1. 学生输入班级的 namespace 和密码
2. 调用 `POST /apps/auth/token` 获取 token
3. 使用返回的 token 访问 KV 存储
4. 如果是学生类型,调用 `POST /apps/tokens/:token/set-student-name` 设置自己的名称
### 场景 2: 管理员配置自动授权
1. 管理员通过账户登录获取 JWT Token
2. 调用 `POST /auto-auth/devices/:uuid/auth-configs` 创建多个授权配置:
- 教师密码deviceType: teacher, isReadOnly: false
- 学生密码deviceType: student, isReadOnly: false
- 家长密码deviceType: parent, isReadOnly: true
3. 学生/教师/家长使用对应密码通过 namespace 登录
**注意**: 设备必须已绑定到管理员的账户才能配置 AutoAuth
---
## 注意事项
1. **密码安全**: 所有密码都使用 bcrypt 进行哈希存储
2. **唯一性约束**:
- 同一设备的 namespace 必须唯一
- 同一设备的 AutoAuth 密码必须唯一(包括 null
3. **级联删除**: 删除设备会级联删除所有相关的 AutoAuth 配置和 AppInstall 记录
4. **只读限制**: isReadOnly 为 true 的 token 在 KV 操作中会受到写入限制
5. **账户绑定要求**: 只有已绑定账户的设备才能管理 AutoAuth 配置,未绑定账户的设备无法使用 AutoAuth 管理接口

0
API_QUICK_REFERENCE.md Normal file
View File

View File

@ -1,4 +1,4 @@
FROM node:24-alpine FROM node:22-alpine
WORKDIR /app WORKDIR /app

129
MIGRATION_CHECKLIST.md Normal file
View File

@ -0,0 +1,129 @@
# Refresh Token系统迁移检查清单
## 🔧 服务端迁移
### 数据库
- [ ] 运行Prisma迁移: `npx prisma migrate dev --name add_refresh_token_system`
- [ ] 验证Account表新增字段: refreshToken, refreshTokenExpiry, tokenVersion
### 环境配置
- [ ] 添加 `ACCESS_TOKEN_EXPIRES_IN=15m`
- [ ] 添加 `REFRESH_TOKEN_EXPIRES_IN=7d`
- [ ] 添加 `REFRESH_TOKEN_SECRET=your-refresh-token-secret`
- [ ] 可选配置RSA密钥对
### 代码验证
- [ ] `utils/tokenManager.js` 文件已创建
- [ ] `utils/jwt.js` 已更新(保持向后兼容)
- [ ] `middleware/jwt-auth.js` 已升级
- [ ] `routes/accounts.js` 新增refresh相关端点
## 🖥️ 前端迁移
### OAuth回调处理
- [ ] 更新回调URL参数解析支持access_token和refresh_token
- [ ] 保持对旧版token参数的兼容性
- [ ] 实现TokenManager类
### Token管理
- [ ] 实现Token刷新逻辑
- [ ] 添加请求拦截器检查X-New-Access-Token响应头
- [ ] 实现401错误自动重试机制
- [ ] 添加登出功能(单设备/全设备)
### 存储策略
- [ ] Access Token存储localStorage/sessionStorage
- [ ] Refresh Token安全存储
- [ ] 实现Token清理逻辑
## 🧪 测试验证
### 功能测试
- [ ] OAuth登录流程测试
- [ ] Token自动刷新测试
- [ ] 手动refresh接口测试
- [ ] 登出功能测试(单设备)
- [ ] 登出功能测试(全设备)
- [ ] Token信息查看测试
### 兼容性测试
- [ ] 旧版JWT token仍然有效
- [ ] 新旧token混合使用场景
- [ ] API向后兼容性验证
### 错误处理测试
- [ ] 过期token处理
- [ ] 无效refresh token处理
- [ ] 网络错误重试
- [ ] 并发刷新场景
## 📊 监控配置
### 日志记录
- [ ] Token生成日志
- [ ] Token刷新日志
- [ ] 认证失败日志
- [ ] 登出操作日志
### 性能监控
- [ ] Token刷新频率统计
- [ ] API响应时间监控
- [ ] 数据库查询性能
## 🔒 安全检查
### Token安全
- [ ] 密钥强度验证
- [ ] Token过期时间配置合理
- [ ] HTTPS传输确认
- [ ] 敏感信息不在日志中暴露
### 访问控制
- [ ] Token撤销功能正常
- [ ] 版本控制机制有效
- [ ] 设备隔离正确
## 📚 文档检查
- [ ] API文档已更新
- [ ] 前端集成指南已提供
- [ ] 迁移步骤文档完整
- [ ] 错误处理指南清晰
## 🚀 上线准备
### 部署前
- [ ] 代码review完成
- [ ] 单元测试通过
- [ ] 集成测试通过
- [ ] 性能测试通过
### 部署时
- [ ] 数据库迁移执行
- [ ] 环境变量配置
- [ ] 服务重启验证
- [ ] 健康检查通过
### 部署后
- [ ] 新用户登录测试
- [ ] 现有用户功能正常
- [ ] 监控指标正常
- [ ] 错误日志检查
## 🔄 回滚计划
### 紧急回滚
- [ ] 回滚代码到上一版本
- [ ] 恢复原环境变量
- [ ] 数据库回滚方案(如需要)
### 数据迁移回滚
- [ ] 备份新增字段数据
- [ ] 移除新增字段的迁移脚本
- [ ] 验证旧版功能正常
---
**检查完成人员**: ___________
**检查完成时间**: ___________
**环境**: [ ] 开发 [ ] 测试 [ ] 生产

0
NEW_APIS_SUMMARY.md Normal file
View File

489
REFRESH_TOKEN_API.md Normal file
View File

@ -0,0 +1,489 @@
# Refresh Token系统API文档
## 概述
ClassworksKV现在支持标准的Refresh Token认证系统提供更安全的用户认证机制。新系统包含
- **Access Token**: 短期令牌默认15分钟用于API访问
- **Refresh Token**: 长期令牌默认7天用于刷新Access Token
- **Token版本控制**: 支持令牌失效和安全登出
- **向后兼容**: 支持旧版JWT令牌
## 配置选项
可以通过环境变量配置token系统
```bash
# Access Token配置
ACCESS_TOKEN_EXPIRES_IN=15m # Access Token过期时间
REFRESH_TOKEN_EXPIRES_IN=7d # Refresh Token过期时间
# 密钥配置HS256算法
JWT_SECRET=your-access-token-secret
REFRESH_TOKEN_SECRET=your-refresh-token-secret
# RSA密钥配置RS256算法可选
JWT_ALG=RS256
ACCESS_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..."
ACCESS_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n..."
REFRESH_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..."
REFRESH_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n..."
```
## API端点
### 1. OAuth登录回调
OAuth登录成功后系统会返回令牌对。
**回调URL参数新版**
```
https://your-frontend.com/?access_token=eyJ...&refresh_token=eyJ...&expires_in=15m&success=true&provider=github
```
**旧版兼容参数:**
```
https://your-frontend.com/?token=eyJ...&success=true&provider=github
```
### 2. 刷新访问令牌
当Access Token即将过期或已过期时使用Refresh Token获取新的Access Token。
**端点:** `POST /api/accounts/refresh`
**请求体:**
```json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
**响应(成功):**
```json
{
"success": true,
"message": "令牌刷新成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": "15m",
"account": {
"id": "clxxxx",
"provider": "github",
"email": "user@example.com",
"name": "User Name",
"avatarUrl": "https://..."
}
}
}
```
**错误响应:**
```json
{
"success": false,
"message": "刷新令牌已过期"
}
```
**错误状态码:**
- `400`: 缺少刷新令牌
- `401`: 无效的刷新令牌、令牌已过期、账户不存在、令牌版本不匹配
### 3. 登出(当前设备)
撤销当前设备的Refresh Token但不影响其他设备。
**端点:** `POST /api/accounts/logout`
**请求头:**
```
Authorization: Bearer <access_token>
```
**响应:**
```json
{
"success": true,
"message": "登出成功"
}
```
### 4. 登出所有设备
撤销账户的所有令牌,强制所有设备重新登录。
**端点:** `POST /api/accounts/logout-all`
**请求头:**
```
Authorization: Bearer <access_token>
```
**响应:**
```json
{
"success": true,
"message": "已从所有设备登出"
}
```
### 5. 获取令牌信息
查看当前令牌的详细信息和状态。
**端点:** `GET /api/accounts/token-info`
**请求头:**
```
Authorization: Bearer <access_token>
```
**响应:**
```json
{
"success": true,
"data": {
"accountId": "clxxxx",
"tokenType": "access",
"tokenVersion": 1,
"issuedAt": "2024-11-01T08:00:00.000Z",
"expiresAt": "2024-11-01T08:15:00.000Z",
"expiresIn": 900,
"isExpired": false,
"isLegacyToken": false,
"hasRefreshToken": true,
"refreshTokenExpiry": "2024-11-08T08:00:00.000Z"
}
}
```
## 自动刷新机制
### 响应头刷新
当Access Token剩余有效期少于5分钟时系统会在响应头中提供新的Access Token
**响应头:**
```
X-New-Access-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Token-Refreshed: true
```
前端应检查这些响应头并更新本地存储的token。
## 前端集成示例
### JavaScript/TypeScript
```javascript
class TokenManager {
constructor() {
this.accessToken = localStorage.getItem('access_token');
this.refreshToken = localStorage.getItem('refresh_token');
}
// 设置令牌对
setTokens(accessToken, refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
}
// 清除令牌
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
}
// 刷新访问令牌
async refreshAccessToken() {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
try {
const response = await fetch('/api/accounts/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh_token: this.refreshToken
}),
});
const data = await response.json();
if (data.success) {
this.accessToken = data.data.access_token;
localStorage.setItem('access_token', this.accessToken);
return this.accessToken;
} else {
throw new Error(data.message);
}
} catch (error) {
this.clearTokens();
throw error;
}
}
// API请求拦截器
async request(url, options = {}) {
const headers = {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
...options.headers,
};
const response = await fetch(url, {
...options,
headers,
});
// 检查是否有新的访问令牌
const newAccessToken = response.headers.get('X-New-Access-Token');
if (newAccessToken) {
this.accessToken = newAccessToken;
localStorage.setItem('access_token', newAccessToken);
}
// 如果token过期尝试刷新
if (response.status === 401) {
try {
await this.refreshAccessToken();
// 重试请求
return this.request(url, options);
} catch (refreshError) {
// 刷新失败,重定向到登录页
window.location.href = '/login';
throw refreshError;
}
}
return response;
}
// 登出
async logout() {
try {
await this.request('/api/accounts/logout', {
method: 'POST',
});
} catch (error) {
console.error('Logout error:', error);
} finally {
this.clearTokens();
window.location.href = '/login';
}
}
// 登出所有设备
async logoutAll() {
try {
await this.request('/api/accounts/logout-all', {
method: 'POST',
});
} catch (error) {
console.error('Logout all error:', error);
} finally {
this.clearTokens();
window.location.href = '/login';
}
}
}
// 使用示例
const tokenManager = new TokenManager();
// OAuth回调处理
function handleOAuthCallback() {
const params = new URLSearchParams(window.location.search);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
if (accessToken && refreshToken) {
tokenManager.setTokens(accessToken, refreshToken);
// 重定向到应用主页
window.location.href = '/dashboard';
} else {
// 处理旧版回调
const legacyToken = params.get('token');
if (legacyToken) {
tokenManager.setTokens(legacyToken, null);
window.location.href = '/dashboard';
}
}
}
// API调用示例
async function getUserProfile() {
try {
const response = await tokenManager.request('/api/accounts/profile');
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to get user profile:', error);
throw error;
}
}
```
### React Hook
```jsx
import { useState, useCallback, useEffect } from 'react';
export function useAuth() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const tokenManager = new TokenManager();
const checkAuth = useCallback(async () => {
if (!tokenManager.accessToken) {
setIsAuthenticated(false);
setLoading(false);
return;
}
try {
const response = await tokenManager.request('/api/accounts/profile');
const data = await response.json();
if (data.success) {
setUser(data.data);
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
} catch (error) {
setIsAuthenticated(false);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
checkAuth();
}, [checkAuth]);
const login = useCallback((accessToken, refreshToken) => {
tokenManager.setTokens(accessToken, refreshToken);
setIsAuthenticated(true);
checkAuth();
}, [checkAuth]);
const logout = useCallback(async () => {
await tokenManager.logout();
setIsAuthenticated(false);
setUser(null);
}, []);
return {
isAuthenticated,
user,
loading,
login,
logout,
tokenManager,
};
}
```
## 安全考虑
### 1. Token存储
- **Access Token**: 可以存储在内存或localStorage中
- **Refresh Token**: 建议存储在httpOnly cookie中需要后端支持或者安全的本地存储
### 2. HTTPS
- 生产环境必须使用HTTPS传输令牌
### 3. Token轮换
- 系统支持令牌版本控制,可以快速失效所有令牌
### 4. 过期时间
- Access Token短期有效15分钟
- Refresh Token长期有效7天
- 可根据安全需求调整
## 迁移指南
### 从旧版JWT系统迁移
1. **前端更新**
- 更新OAuth回调处理逻辑
- 实现Token刷新机制
- 处理新的响应头
2. **向后兼容**
- 旧版JWT token仍然有效
- 系统会在响应中标记`isLegacyToken: true`
- 建议用户重新登录获取新令牌
3. **数据库迁移**
```bash
# 运行Prisma迁移
npm run prisma migrate dev --name add_refresh_token_support
```
## 错误处理
### 常见错误
| 错误代码 | 错误信息 | 处理方式 |
|---------|---------|---------|
| 401 | JWT token已过期 | 使用refresh token刷新 |
| 401 | 无效的刷新令牌 | 重新登录 |
| 401 | 令牌版本不匹配 | 重新登录 |
| 401 | 账户不存在 | 重新登录 |
### 错误处理流程
```mermaid
graph TD
A[API请求] --> B{Token有效?}
B -->|是| C[返回数据]
B -->|否,401错误| D{有Refresh Token?}
D -->|否| E[重定向登录]
D -->|是| F[尝试刷新Token]
F --> G{刷新成功?}
G -->|是| H[重试原请求]
G -->|否| E
H --> C
```
## 监控和日志
### 建议监控指标
- Token刷新频率
- Token刷新失败率
- 用户登出频率
- 异常登录尝试
### 日志记录
系统会记录以下事件:
- Token生成
- Token刷新
- Token撤销
- 认证失败
## 性能优化
### 1. Token缓存
- 在内存中缓存已验证的token适用于高并发场景
### 2. 数据库优化
- 为`refreshToken`字段添加索引
- 定期清理过期的refresh token
### 3. 前端优化
- 实现Token预刷新机制
- 使用Web Workers处理Token逻辑

112
REFRESH_TOKEN_QUICKSTART.md Normal file
View File

@ -0,0 +1,112 @@
# Refresh Token系统 - 快速使用指南
## 🚀 快速开始
### 1. 环境变量配置
```bash
# 添加到 .env 文件
ACCESS_TOKEN_EXPIRES_IN=15m
REFRESH_TOKEN_EXPIRES_IN=7d
REFRESH_TOKEN_SECRET=your-refresh-token-secret-change-this
```
### 2. 数据库迁移
```bash
npx prisma migrate dev --name add_refresh_token_system
```
### 3. 新的OAuth回调参数
登录成功后回调URL现在包含
```
?access_token=eyJ...&refresh_token=eyJ...&expires_in=15m&success=true
```
## 📝 核心API
### 刷新Token
```http
POST /api/accounts/refresh
Content-Type: application/json
{
"refresh_token": "eyJ..."
}
```
### 登出当前设备
```http
POST /api/accounts/logout
Authorization: Bearer <access_token>
```
### 登出所有设备
```http
POST /api/accounts/logout-all
Authorization: Bearer <access_token>
```
## 💻 前端集成
### 基础Token管理
```javascript
class TokenManager {
setTokens(accessToken, refreshToken) {
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
}
async refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
const response = await fetch('/api/accounts/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
});
const data = await response.json();
if (data.success) {
localStorage.setItem('access_token', data.data.access_token);
return data.data.access_token;
}
throw new Error(data.message);
}
}
```
### 自动刷新拦截器
```javascript
// 检查响应头中的新token
const newToken = response.headers.get('X-New-Access-Token');
if (newToken) {
localStorage.setItem('access_token', newToken);
}
// 401错误时自动刷新
if (response.status === 401) {
await tokenManager.refreshToken();
// 重试请求
}
```
## 🔒 安全特性
- ✅ 短期Access Token15分钟
- ✅ 长期Refresh Token7天
- ✅ Token版本控制
- ✅ 设备级登出
- ✅ 全局登出
- ✅ 自动刷新机制
- ✅ 向后兼容
## 🔄 迁移步骤
1. **更新环境变量**
2. **运行数据库迁移**
3. **更新前端OAuth回调处理**
4. **实现Token刷新逻辑**
5. **测试登出功能**
详细文档请参考:`REFRESH_TOKEN_API.md`

174
REFRESH_TOKEN_SUMMARY.md Normal file
View File

@ -0,0 +1,174 @@
# 账户登录密钥系统重构完成报告
## 📋 项目概述
已成功重构ClassworksKV的账户登录密钥系统从单一JWT令牌升级为标准的Refresh Token系统大幅提升了安全性和用户体验。
## ✅ 完成的工作
### 1. 数据库架构更新
- 在`Account`模型中添加了`refreshToken``refreshTokenExpiry``tokenVersion`字段
- 支持令牌版本控制,可快速失效所有设备的令牌
- 向后兼容现有数据
### 2. 核心Token管理系统
- **创建 `utils/tokenManager.js`**: 全新的令牌管理核心
- 生成Access Token15分钟有效期
- 生成Refresh Token7天有效期
- 支持HS256和RS256算法
- 令牌刷新和撤销功能
- 安全验证机制
- **重构 `utils/jwt.js`**: 保持向后兼容性
- 重新导出新的令牌管理功能
- 保留旧版API供现有代码使用
### 3. 认证中间件升级
- **更新 `middleware/jwt-auth.js`**:
- 支持新的Access Token验证
- 自动检测即将过期的令牌并在响应头提供新令牌
- 向后兼容旧版JWT令牌
- 新增可选认证中间件
### 4. API端点扩展
- **更新 `routes/accounts.js`**:
- OAuth回调现在返回令牌对access_token + refresh_token
- 新增 `/api/accounts/refresh` - 刷新访问令牌
- 新增 `/api/accounts/logout` - 单设备登出
- 新增 `/api/accounts/logout-all` - 全设备登出
- 新增 `/api/accounts/token-info` - 查看令牌状态
### 5. 安全特性
- **短期Access Token**: 默认15分钟降低泄露风险
- **长期Refresh Token**: 默认7天用户体验友好
- **令牌版本控制**: 支持立即失效所有设备的令牌
- **自动刷新机制**: 在令牌即将过期时自动提供新令牌
- **设备级管理**: 支持单设备或全设备登出
## 📚 文档输出
### 1. 详细API文档
**文件**: `REFRESH_TOKEN_API.md`
- 完整的API接口说明
- 前端集成示例JavaScript/React
- 安全考虑和最佳实践
- 错误处理指南
- 性能优化建议
### 2. 快速使用指南
**文件**: `REFRESH_TOKEN_QUICKSTART.md`
- 环境配置说明
- 核心API使用方法
- 前端集成代码示例
- 迁移步骤指导
## 🔧 配置说明
### 环境变量
```bash
# Access Token配置
ACCESS_TOKEN_EXPIRES_IN=15m # 访问令牌过期时间
REFRESH_TOKEN_EXPIRES_IN=7d # 刷新令牌过期时间
# 密钥配置
JWT_SECRET=your-access-token-secret # Access Token密钥
REFRESH_TOKEN_SECRET=your-refresh-token-secret # Refresh Token密钥
# 可选RSA算法配置
JWT_ALG=RS256
ACCESS_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----..."
ACCESS_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----..."
REFRESH_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----..."
REFRESH_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----..."
```
## 🚀 部署步骤
### 1. 数据库迁移
```bash
npx prisma migrate dev --name add_refresh_token_system
```
### 2. 环境变量更新
```bash
# 添加新的环境变量到 .env 文件
echo "ACCESS_TOKEN_EXPIRES_IN=15m" >> .env
echo "REFRESH_TOKEN_EXPIRES_IN=7d" >> .env
echo "REFRESH_TOKEN_SECRET=your-refresh-token-secret-change-this" >> .env
```
### 3. 前端更新
- 更新OAuth回调处理逻辑
- 实现Token刷新机制
- 添加自动重试逻辑
## 🔄 向后兼容性
- ✅ 现有JWT令牌继续有效
- ✅ 旧版API端点保持不变
- ✅ 渐进式迁移支持
- ✅ 中间件自动检测令牌类型
## 📊 系统架构
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 前端应用 │ │ ClassworksKV │ │ 数据库 │
│ │ │ 服务端 │ │ │
├─────────────────┤ ├──────────────────┤ ├─────────────────┤
│ • Token存储 │◄──►│ • OAuth认证 │◄──►│ • Account表 │
│ • 自动刷新 │ │ • Token生成 │ │ • refreshToken │
│ • 请求拦截 │ │ • Token验证 │ │ • tokenVersion │
│ • 错误处理 │ │ • Token刷新 │ │ • 过期时间 │
└─────────────────┘ └──────────────────┘ └─────────────────┘
```
## 🛡️ 安全增强
### 改进前(旧系统)
- 单一JWT令牌
- 长期有效7天
- 泄露风险高
- 无法远程登出
### 改进后(新系统)
- 双令牌系统
- Access Token短期15分钟
- Refresh Token长期7天
- 令牌版本控制
- 设备级管理
- 自动刷新机制
## 📈 性能考虑
- **数据库**: 为refreshToken字段添加索引
- **内存**: Token缓存机制可选
- **网络**: 预刷新机制减少延迟
- **存储**: 定期清理过期令牌
## 🧪 测试建议
### 功能测试
1. OAuth登录流程测试
2. Token刷新功能测试
3. 登出功能测试
4. 过期处理测试
### 安全测试
1. 令牌篡改测试
2. 过期令牌测试
3. 并发刷新测试
4. 版本不匹配测试
## 📞 后续支持
- 监控令牌刷新频率
- 分析用户登录模式
- 优化过期时间配置
- 收集用户反馈
---
**重构完成时间**: 2025年11月1日
**文档版本**: v1.0
**兼容性**: 向后兼容,支持渐进式迁移

565
SOCKET_API.md Normal file
View File

@ -0,0 +1,565 @@
# Socket.IO 实时频道接口文档(前端)
## 概述
ClassworksKV 提供基于 Socket.IO 的实时键值变更通知服务。前端使用 **KV token**(应用安装 token加入频道服务端会自动将 token 映射到对应设备的 uuid 房间。**同一设备的不同 token 会被归入同一频道**,因此多个客户端/应用可以共享实时更新。
**重要变更**:不再支持直接使用 uuid 加入频道,所有连接必须使用有效的 KV token。
## 安装依赖
前端项目安装 Socket.IO 客户端:
```bash
# npm
npm install socket.io-client
# pnpm
pnpm add socket.io-client
# yarn
yarn add socket.io-client
```
## 连接服务器
### 基础连接
```typescript
import { io, Socket } from 'socket.io-client';
const SERVER_URL = 'http://localhost:3000'; // 替换为实际服务器地址
const socket: Socket = io(SERVER_URL, {
transports: ['websocket'],
});
```
### 连接时自动加入频道(推荐)
在连接握手时通过 query 参数传入 token自动加入对应设备频道
```typescript
const socket = io(SERVER_URL, {
transports: ['websocket'],
query: {
token: '<your-kv-app-token>', // 或使用 apptoken 参数
},
});
// 监听加入成功
socket.on('joined', (info) => {
console.log('已加入频道:', info);
// { by: 'token', uuid: 'device-uuid-xxx' }
});
// 监听加入失败
socket.on('join-error', (error) => {
console.error('加入频道失败:', error);
// { by: 'token', reason: 'invalid_token' }
});
```
## 事件接口
### 1. 客户端发送的事件
#### `join-token` - 使用 token 加入频道
连接后按需加入频道。
**载荷格式:**
```typescript
{
token?: string; // KV token二选一
apptoken?: string; // 或使用 apptoken 字段
}
```
**示例:**
```typescript
socket.emit('join-token', { token: '<your-kv-app-token>' });
```
---
#### `leave-token` - 使用 token 离开频道
离开指定 token 对应的设备频道。
**载荷格式:**
```typescript
{
token?: string;
apptoken?: string;
}
```
**示例:**
```typescript
socket.emit('leave-token', { token: '<your-kv-app-token>' });
```
---
#### `leave-all` - 离开所有频道
断开前清理,离开该连接加入的所有设备频道。
**载荷:** 无
**示例:**
```typescript
socket.emit('leave-all');
```
---
### 2. 服务端发送的事件
#### `joined` - 加入成功通知
当成功加入频道后,服务端会发送此事件。
**载荷格式:**
```typescript
{
by: 'token';
uuid: string; // 设备 uuid用于调试/日志)
}
```
**示例:**
```typescript
socket.on('joined', (info) => {
console.log(`成功加入设备 ${info.uuid} 的频道`);
});
```
---
#### `join-error` - 加入失败通知
token 无效或查询失败时触发。
**载荷格式:**
```typescript
{
by: 'token';
reason: 'invalid_token'; // 失败原因
}
```
**示例:**
```typescript
socket.on('join-error', (error) => {
console.error('Token 无效,无法加入频道');
});
```
---
#### `kv-key-changed` - 键值变更广播
当设备下的 KV 键被创建/更新/删除时,向该设备频道内所有连接广播此事件。
**载荷格式:**
```typescript
{
uuid: string; // 设备 uuid
key: string; // 变更的键名
action: 'upsert' | 'delete'; // 操作类型
// 仅 action='upsert' 时存在:
created?: boolean; // 是否首次创建
updatedAt?: string; // 更新时间ISO 8601
batch?: boolean; // 是否为批量导入中的单条
// 仅 action='delete' 时存在:
deletedAt?: string; // 删除时间ISO 8601
}
```
**示例:**
```typescript
socket.on('kv-key-changed', (msg) => {
if (msg.action === 'upsert') {
console.log(`键 ${msg.key} 已${msg.created ? '创建' : '更新'}`);
// 刷新本地缓存或重新获取数据
} else if (msg.action === 'delete') {
console.log(`键 ${msg.key} 已删除`);
// 从本地缓存移除
}
});
```
**载荷示例:**
- 新建/更新键:
```json
{
"uuid": "device-001",
"key": "settings/theme",
"action": "upsert",
"created": false,
"updatedAt": "2025-10-25T08:30:00.000Z"
}
```
- 删除键:
```json
{
"uuid": "device-001",
"key": "settings/theme",
"action": "delete",
"deletedAt": "2025-10-25T08:35:00.000Z"
}
```
- 批量导入中的单条:
```json
{
"uuid": "device-001",
"key": "config/version",
"action": "upsert",
"created": true,
"updatedAt": "2025-10-25T08:40:00.000Z",
"batch": true
}
```
---
#### `device-joined` - 设备频道连接数变化(可选)
当有新连接加入某设备频道时广播,用于显示在线人数。
**载荷格式:**
```typescript
{
uuid: string; // 设备 uuid
connections: number; // 当前连接数
}
```
**示例:**
```typescript
socket.on('device-joined', (info) => {
console.log(`设备 ${info.uuid} 当前有 ${info.connections} 个连接`);
});
```
---
## 完整使用示例
### React Hook 封装
```typescript
import { useEffect, useRef } from 'react';
import { io, Socket } from 'socket.io-client';
const SERVER_URL = import.meta.env.VITE_SERVER_URL || 'http://localhost:3000';
interface KvKeyChange {
uuid: string;
key: string;
action: 'upsert' | 'delete';
created?: boolean;
updatedAt?: string;
deletedAt?: string;
batch?: boolean;
}
export function useKvChannel(
token: string | null,
onKeyChanged?: (event: KvKeyChange) => void
) {
const socketRef = useRef<Socket | null>(null);
useEffect(() => {
if (!token) return;
// 创建连接并加入频道
const socket = io(SERVER_URL, {
transports: ['websocket'],
query: { token },
});
socket.on('joined', (info) => {
console.log('已加入设备频道:', info.uuid);
});
socket.on('join-error', (err) => {
console.error('加入频道失败:', err.reason);
});
socket.on('kv-key-changed', (msg: KvKeyChange) => {
onKeyChanged?.(msg);
});
socketRef.current = socket;
return () => {
socket.emit('leave-all');
socket.close();
};
}, [token]);
return socketRef.current;
}
```
### Vue Composable 封装
```typescript
import { ref, watch, onUnmounted } from 'vue';
import { io, Socket } from 'socket.io-client';
const SERVER_URL = import.meta.env.VITE_SERVER_URL || 'http://localhost:3000';
export function useKvChannel(token: Ref<string | null>) {
const socket = ref<Socket | null>(null);
const isConnected = ref(false);
const deviceUuid = ref<string | null>(null);
watch(token, (newToken) => {
// 清理旧连接
if (socket.value) {
socket.value.emit('leave-all');
socket.value.close();
socket.value = null;
}
if (!newToken) return;
// 创建新连接
const s = io(SERVER_URL, {
transports: ['websocket'],
query: { token: newToken },
});
s.on('connect', () => {
isConnected.value = true;
});
s.on('disconnect', () => {
isConnected.value = false;
});
s.on('joined', (info) => {
deviceUuid.value = info.uuid;
console.log('已加入设备频道:', info.uuid);
});
s.on('join-error', (err) => {
console.error('加入失败:', err.reason);
});
socket.value = s;
}, { immediate: true });
onUnmounted(() => {
if (socket.value) {
socket.value.emit('leave-all');
socket.value.close();
}
});
return { socket, isConnected, deviceUuid };
}
```
### 使用示例React
```tsx
import { useKvChannel } from './hooks/useKvChannel';
function MyComponent() {
const token = localStorage.getItem('kv-token');
useKvChannel(token, (event) => {
console.log('KV 变更:', event);
if (event.action === 'upsert') {
// 更新本地状态或重新获取数据
fetchKeyValue(event.key);
} else if (event.action === 'delete') {
// 从本地移除
removeFromCache(event.key);
}
});
return <div>实时监听中...</div>;
}
```
---
## REST API查询在线设备
除了 Socket.IO 实时事件,还提供 HTTP 接口查询当前在线设备列表。
### `GET /devices/online`
**响应格式:**
```typescript
{
success: true;
devices: Array<{
uuid: string; // 设备 uuid
connections: number; // 当前连接数
name: string | null; // 设备名称(若已设置)
}>;
}
```
**示例:**
```typescript
const response = await fetch(`${SERVER_URL}/devices/online`);
const data = await response.json();
console.log('在线设备:', data.devices);
// [{ uuid: 'device-001', connections: 3, name: 'My Device' }, ...]
```
---
## 获取 KV Token
前端需要先获取有效的 KV token 才能加入频道。Token 通过以下接口获取:
### 安装应用获取 token
**接口:** `POST /apps/devices/:uuid/install/:appId`
**认证:** 需要设备 UUID 认证(密码或账户 JWT
**响应包含:**
```typescript
{
id: string;
appId: string;
token: string; // 用于 KV 操作和加入频道
note: string | null;
name: string | null; // 等同于 note便于展示
installedAt: string;
}
```
### 列出设备已有的 token
**接口:** `GET /apps/tokens?uuid=<device-uuid>`
**响应:**
```typescript
{
success: true;
tokens: Array<{
id: string;
token: string;
appId: string;
installedAt: string;
note: string | null;
name: string | null; // 等同于 note
}>;
deviceUuid: string;
}
```
---
## 注意事项与最佳实践
1. **Token 必需**:所有连接必须提供有效的 KV token不再支持直接使用 uuid。
2. **频道归并**:同一设备的不同 token 会自动归入同一房间(以设备 uuid 为房间名),因此多个应用/客户端可以共享实时更新。
3. **连接管理**
- 组件卸载时调用 `leave-all``leave-token` 清理连接
- 避免频繁创建/销毁连接,建议在应用全局维护单个 socket 实例
4. **重连处理**
- Socket.IO 客户端内置自动重连
- 在 `connect` 事件后重新 emit `join-token` 确保重连后仍在频道内(或在握手时传 token 自动加入)
5. **CORS 配置**
- 服务端通过环境变量 `FRONTEND_URL` 控制允许的来源
- 未设置时默认为 `*`(允许所有来源)
- 生产环境建议设置为前端实际域名
6. **错误处理**
- 监听 `join-error` 事件处理 token 无效情况
- 监听 `connect_error` 处理网络连接失败
7. **性能优化**
- 批量导入时会逐条广播,前端可根据 `batch: true` 标记做去抖处理
- 建议在本地维护 KV 缓存,收到变更通知时增量更新而非全量刷新
---
## 环境变量配置
服务端需要配置以下环境变量:
```env
# Socket.IO CORS 允许的来源
FRONTEND_URL=http://localhost:5173
# 服务器端口(可选,默认 3000
PORT=3000
```
---
## 常见问题
### Q: 如何支持多个设备?
A: 对每个设备的 token 分别调用 `join-token`,或在连接时传入一个 token后续通过事件加入其他设备。
```typescript
socket.emit('join-token', { token: token1 });
socket.emit('join-token', { token: token2 });
```
### Q: 广播延迟有多大?
A: 通常在毫秒级取决于网络状况。WebSocket 连接建立后,广播几乎实时。
### Q: Token 过期怎么办?
A: Token 本身不会过期,除非手动删除应用安装记录。如收到 `join-error`,检查 token 是否已被卸载。
### Q: 可以在 Node.js 后端使用吗?
A: 可以,使用相同的 socket.io-client 包,接口完全一致。
---
## 更新日志
### v1.1.0 (2025-10-25)
**破坏性变更:**
- 移除直接使用 uuid 加入频道的接口(`join-device` / `leave-device`
- 现在必须使用 KV token 通过 `join-token` 或握手 query 加入
**新增:**
- `leave-all` 事件:离开所有已加入的频道
- 握手时支持 `token``apptoken` 两种参数名
**改进:**
- 同一设备的不同 token 自动归入同一房间
- 优化在线设备计数准确性
---
## 技术支持
如有问题,请查阅:
- 服务端源码:`utils/socket.js`
- KV 路由:`routes/kv-token.js`
- 设备管理:`routes/device.js`
或提交 Issue 到项目仓库。

View File

@ -18,9 +18,9 @@ export const oauthProviders = {
zerocat: { zerocat: {
clientId: process.env.ZEROCAT_CLIENT_ID, clientId: process.env.ZEROCAT_CLIENT_ID,
clientSecret: process.env.ZEROCAT_CLIENT_SECRET, clientSecret: process.env.ZEROCAT_CLIENT_SECRET,
authorizationURL: "https://api.zcservice.houlang.cloud/oauth/authorize", authorizationURL: "https://zerocat-api.houlang.cloud/oauth/authorize",
tokenURL: "https://api.zcservice.houlang.cloud/oauth/token", tokenURL: "https://zerocat-api.houlang.cloud/oauth/token",
userInfoURL: "https://api.zcservice.houlang.cloud/oauth/userinfo", userInfoURL: "https://zerocat-api.houlang.cloud/oauth/userinfo",
scope: "user:basic user:email", scope: "user:basic user:email",
// 展示相关 // 展示相关
name: "ZeroCat", name: "ZeroCat",

91
docker/schema.prisma Normal file
View File

@ -0,0 +1,91 @@
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
}
model KVStore {
deviceId Int
key String
value Json
creatorIp String? @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关联关系
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
@@id([deviceId, key])
}
model Account {
id String @id @default(cuid())
provider String // OAuth提供者 (例如: google, github, gitlab等)
providerId String // 提供者返回的用户唯一ID
email String? // 用户邮箱
name String? // 用户名称
avatarUrl String? // 用户头像URL
providerData Json? // OAuth提供者返回的完整信息
accessToken String? @db.Text // 账户访问令牌
refreshToken String? @db.Text // 刷新令牌
refreshTokenExpiry DateTime? // 刷新令牌过期时间
tokenVersion Int @default(1) // 令牌版本,用于令牌失效
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关联的设备
devices Device[]
@@unique([provider, providerId]) // 确保同一提供者的用户ID唯一
}
model Device {
id Int @id @default(autoincrement())
uuid String @unique // 设备的唯一标识符
name String?
accountId String? // 关联的账户ID
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
password String?
passwordHint String?
namespace String? @unique // 用户自定义的唯一命名空间
// 关联关系
account Account? @relation(fields: [accountId], references: [id], onDelete: SetNull)
appInstalls AppInstall[]
kvStore KVStore[] // 设备相关的KV存储
autoAuths AutoAuth[] // 自动授权配置
}
model AppInstall {
id String @id @default(cuid())
deviceId Int // 关联的设备ID
appId String // 应用ID (SHA256 hash)
token String @unique // 应用安装的唯一访问令牌,拥有完整KV读写权限
note String? // 安装备注
isReadOnly Boolean @default(false) // 是否只读
deviceType String? // 设备类型: teacher(教师), student(学生), classroom(班级一体机), parent(家长)
installedAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关联关系
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
}
model AutoAuth {
id String @id @default(cuid())
deviceId Int // 关联的设备ID
password String? // 配置密码,可以为空
deviceType String? // 自动设备类型: teacher(教师), student(学生), classroom(班级一体机), parent(家长)
isReadOnly Boolean @default(false) // 是否只读
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关联关系
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
@@unique([deviceId, password]) // 同一设备的密码必须唯一
}

View File

@ -0,0 +1,44 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma-related types and utilities in a browser.
* Use it to get access to models, enums, and input types.
*
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
* See `client.ts` for the standard, server-side entry point.
*
* 🟢 You can import this file directly.
*/
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
export { Prisma }
export * as $Enums from './enums.ts'
export * from './enums.ts';
/**
* Model Account
*
*/
export type Account = Prisma.AccountModel
/**
* Model AppInstall
*
*/
export type AppInstall = Prisma.AppInstallModel
/**
* Model AutoAuth
*
*/
export type AutoAuth = Prisma.AutoAuthModel
/**
* Model Device
*
*/
export type Device = Prisma.DeviceModel
/**
* Model KVStore
*
*/
export type KVStore = Prisma.KVStoreModel

View File

@ -0,0 +1,66 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
*
* 🟢 You can import this file directly.
*/
import * as process from 'node:process'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
import * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums.ts"
import * as $Class from "./internal/class.ts"
import * as Prisma from "./internal/prismaNamespace.ts"
export * as $Enums from './enums.ts'
export * from "./enums.ts"
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Accounts
* const accounts = await prisma.account.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export const PrismaClient = $Class.getPrismaClientClass()
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma }
/**
* Model Account
*
*/
export type Account = Prisma.AccountModel
/**
* Model AppInstall
*
*/
export type AppInstall = Prisma.AppInstallModel
/**
* Model AutoAuth
*
*/
export type AutoAuth = Prisma.AutoAuthModel
/**
* Model Device
*
*/
export type Device = Prisma.DeviceModel
/**
* Model KVStore
*
*/
export type KVStore = Prisma.KVStoreModel

View File

@ -0,0 +1,502 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
*
* 🟢 You can import this file directly.
*/
import type * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums.ts"
import type * as Prisma from "./internal/prismaNamespace.ts"
export type StringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type JsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type DateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
}
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type JsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonFilterBase<$PrismaModel>>, 'path'>>
export type JsonFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type JsonWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedJsonFilter<$PrismaModel>
_max?: Prisma.NestedJsonFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedDateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedJsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type NestedFloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type NestedJsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}

15
generated/prisma/enums.ts Normal file
View File

@ -0,0 +1,15 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports all enum related types from the schema.
*
* 🟢 You can import this file directly.
*/
// This file is empty because there are no enums in the schema.
export {}

View File

@ -0,0 +1,232 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* Please import the `PrismaClient` class from the `client.ts` file instead.
*/
import * as runtime from "@prisma/client/runtime/client"
import type * as Prisma from "./prismaNamespace.ts"
const config: runtime.GetPrismaClientConfig = {
"previewFeatures": [],
"clientVersion": "7.3.0",
"engineVersion": "9d6ad21cbbceab97458517b147a6a09ff43aa735",
"activeProvider": "postgresql",
"inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel Account {\n id String @id(map: \"idx_18303_PRIMARY\") @db.VarChar(191)\n provider String @db.VarChar(191)\n providerId String @db.VarChar(191)\n email String? @db.VarChar(191)\n name String? @db.VarChar(191)\n avatarUrl String? @db.VarChar(191)\n providerData Json? @db.Json\n accessToken String?\n createdAt DateTime @default(now()) @db.Timestamptz(6)\n updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)\n refreshToken String?\n refreshTokenExpiry DateTime? @db.Timestamptz(6)\n tokenVersion Int @default(1)\n\n devices Device[]\n\n @@unique([provider, providerId], map: \"idx_18303_Account_provider_providerId_key\")\n}\n\nmodel AppInstall {\n id String @id(map: \"idx_18310_PRIMARY\") @db.VarChar(191)\n deviceId Int\n appId String @db.VarChar(191)\n token String @unique(map: \"idx_18310_AppInstall_token_key\") @db.VarChar(191)\n note String? @db.VarChar(191)\n installedAt DateTime @default(now()) @db.Timestamptz(6)\n updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)\n deviceType String? @db.VarChar(191)\n isReadOnly Boolean @default(false)\n\n device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)\n\n @@index([deviceId], map: \"idx_18310_AppInstall_deviceId_fkey\")\n}\n\nmodel AutoAuth {\n id String @id(map: \"idx_18317_PRIMARY\") @db.VarChar(191)\n deviceId Int\n password String? @db.VarChar(191)\n deviceType String? @db.VarChar(191)\n isReadOnly Boolean @default(false)\n createdAt DateTime @default(now()) @db.Timestamptz(6)\n updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)\n\n device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)\n\n @@unique([deviceId, password], map: \"idx_18317_AutoAuth_deviceId_password_key\")\n}\n\nmodel Device {\n id Int @id(map: \"idx_18324_PRIMARY\")\n uuid String @unique(map: \"idx_18324_Device_uuid_key\") @db.VarChar(191)\n name String? @db.VarChar(191)\n accountId String? @db.VarChar(191)\n createdAt DateTime @default(now()) @db.Timestamptz(6)\n updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)\n password String? @db.VarChar(191)\n passwordHint String? @db.VarChar(191)\n namespace String? @unique(map: \"idx_18324_Device_namespace_key\") @db.VarChar(191)\n\n // 关联关系\n account Account? @relation(fields: [accountId], references: [id], onDelete: SetNull)\n appInstalls AppInstall[]\n kvStore KVStore[] // 设备相关的KV存储\n autoAuths AutoAuth[] // 自动授权配置\n\n @@index([accountId], map: \"idx_18324_Device_accountId_fkey\")\n}\n\nmodel KVStore {\n deviceId Int\n key String @db.VarChar(191)\n value Json @db.Json\n creatorIp String? @default(\"\") @db.VarChar(191)\n createdAt DateTime @default(now()) @db.Timestamptz(6)\n updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)\n\n device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)\n\n @@id([deviceId, key], map: \"idx_18330_PRIMARY\")\n}\n",
"runtimeDataModel": {
"models": {},
"enums": {},
"types": {}
}
}
config.runtimeDataModel = JSON.parse("{\"models\":{\"Account\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"provider\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"avatarUrl\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"accessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshTokenExpiry\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"tokenVersion\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"devices\",\"kind\":\"object\",\"type\":\"Device\",\"relationName\":\"AccountToDevice\"}],\"dbName\":null},\"AppInstall\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deviceId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"note\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"installedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"deviceType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isReadOnly\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"device\",\"kind\":\"object\",\"type\":\"Device\",\"relationName\":\"AppInstallToDevice\"}],\"dbName\":null},\"AutoAuth\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deviceId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deviceType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isReadOnly\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"device\",\"kind\":\"object\",\"type\":\"Device\",\"relationName\":\"AutoAuthToDevice\"}],\"dbName\":null},\"Device\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"uuid\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"passwordHint\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"namespace\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToDevice\"},{\"name\":\"appInstalls\",\"kind\":\"object\",\"type\":\"AppInstall\",\"relationName\":\"AppInstallToDevice\"},{\"name\":\"kvStore\",\"kind\":\"object\",\"type\":\"KVStore\",\"relationName\":\"DeviceToKVStore\"},{\"name\":\"autoAuths\",\"kind\":\"object\",\"type\":\"AutoAuth\",\"relationName\":\"AutoAuthToDevice\"}],\"dbName\":null},\"KVStore\":{\"fields\":[{\"name\":\"deviceId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"creatorIp\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"device\",\"kind\":\"object\",\"type\":\"Device\",\"relationName\":\"DeviceToKVStore\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
const { Buffer } = await import('node:buffer')
const wasmArray = Buffer.from(wasmBase64, 'base64')
return new WebAssembly.Module(wasmArray)
}
config.compilerWasm = {
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.mjs"),
getQueryCompilerWasmModule: async () => {
const { wasm } = await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.wasm-base64.mjs")
return await decodeBase64AsWasm(wasm)
},
importName: "./query_compiler_fast_bg.js"
}
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
export interface PrismaClientConstructor {
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Accounts
* const accounts = await prisma.account.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
new <
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
LogOpts extends LogOptions<Options> = LogOptions<Options>,
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
}
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Accounts
* const accounts = await prisma.account.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export interface PrismaClient<
in LogOpts extends Prisma.LogLevel = never,
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
> {
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
/**
* Connect with the database
*/
$connect(): runtime.Types.Utils.JsPromise<void>;
/**
* Disconnect from the database
*/
$disconnect(): runtime.Types.Utils.JsPromise<void>;
/**
* Executes a prepared raw query and returns the number of affected rows.
* @example
* ```
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
/**
* Executes a raw query and returns the number of affected rows.
* Susceptible to SQL injections, see documentation.
* @example
* ```
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
/**
* Performs a prepared raw query and returns the `SELECT` data.
* @example
* ```
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
/**
* Performs a raw query and returns the `SELECT` data.
* Susceptible to SQL injections, see documentation.
* @example
* ```
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
/**
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
* @example
* ```
* const [george, bob, alice] = await prisma.$transaction([
* prisma.user.create({ data: { name: 'George' } }),
* prisma.user.create({ data: { name: 'Bob' } }),
* prisma.user.create({ data: { name: 'Alice' } }),
* ])
* ```
*
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
*/
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
extArgs: ExtArgs
}>>
/**
* `prisma.account`: Exposes CRUD operations for the **Account** model.
* Example usage:
* ```ts
* // Fetch zero or more Accounts
* const accounts = await prisma.account.findMany()
* ```
*/
get account(): Prisma.AccountDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.appInstall`: Exposes CRUD operations for the **AppInstall** model.
* Example usage:
* ```ts
* // Fetch zero or more AppInstalls
* const appInstalls = await prisma.appInstall.findMany()
* ```
*/
get appInstall(): Prisma.AppInstallDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.autoAuth`: Exposes CRUD operations for the **AutoAuth** model.
* Example usage:
* ```ts
* // Fetch zero or more AutoAuths
* const autoAuths = await prisma.autoAuth.findMany()
* ```
*/
get autoAuth(): Prisma.AutoAuthDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.device`: Exposes CRUD operations for the **Device** model.
* Example usage:
* ```ts
* // Fetch zero or more Devices
* const devices = await prisma.device.findMany()
* ```
*/
get device(): Prisma.DeviceDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.kVStore`: Exposes CRUD operations for the **KVStore** model.
* Example usage:
* ```ts
* // Fetch zero or more KVStores
* const kVStores = await prisma.kVStore.findMany()
* ```
*/
get kVStore(): Prisma.KVStoreDelegate<ExtArgs, { omit: OmitOpts }>;
}
export function getPrismaClientClass(): PrismaClientConstructor {
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,197 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/index-browser"
export type * from '../models.ts'
export type * from './prismaNamespace.ts'
export const Decimal = runtime.Decimal
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
export const ModelName = {
Account: 'Account',
AppInstall: 'AppInstall',
AutoAuth: 'AutoAuth',
Device: 'Device',
KVStore: 'KVStore'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
/*
* Enums
*/
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const AccountScalarFieldEnum = {
id: 'id',
provider: 'provider',
providerId: 'providerId',
email: 'email',
name: 'name',
avatarUrl: 'avatarUrl',
providerData: 'providerData',
accessToken: 'accessToken',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
refreshToken: 'refreshToken',
refreshTokenExpiry: 'refreshTokenExpiry',
tokenVersion: 'tokenVersion'
} as const
export type AccountScalarFieldEnum = (typeof AccountScalarFieldEnum)[keyof typeof AccountScalarFieldEnum]
export const AppInstallScalarFieldEnum = {
id: 'id',
deviceId: 'deviceId',
appId: 'appId',
token: 'token',
note: 'note',
installedAt: 'installedAt',
updatedAt: 'updatedAt',
deviceType: 'deviceType',
isReadOnly: 'isReadOnly'
} as const
export type AppInstallScalarFieldEnum = (typeof AppInstallScalarFieldEnum)[keyof typeof AppInstallScalarFieldEnum]
export const AutoAuthScalarFieldEnum = {
id: 'id',
deviceId: 'deviceId',
password: 'password',
deviceType: 'deviceType',
isReadOnly: 'isReadOnly',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type AutoAuthScalarFieldEnum = (typeof AutoAuthScalarFieldEnum)[keyof typeof AutoAuthScalarFieldEnum]
export const DeviceScalarFieldEnum = {
id: 'id',
uuid: 'uuid',
name: 'name',
accountId: 'accountId',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
password: 'password',
passwordHint: 'passwordHint',
namespace: 'namespace'
} as const
export type DeviceScalarFieldEnum = (typeof DeviceScalarFieldEnum)[keyof typeof DeviceScalarFieldEnum]
export const KVStoreScalarFieldEnum = {
deviceId: 'deviceId',
key: 'key',
value: 'value',
creatorIp: 'creatorIp',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type KVStoreScalarFieldEnum = (typeof KVStoreScalarFieldEnum)[keyof typeof KVStoreScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const JsonNullValueInput = {
JsonNull: JsonNull
} as const
export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
} as const
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const JsonNullValueFilter = {
DbNull: DbNull,
JsonNull: JsonNull,
AnyNull: AnyNull
} as const
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]

View File

@ -0,0 +1,16 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This is a barrel export file for all models and their related types.
*
* 🟢 You can import this file directly.
*/
export type * from './models/Account.ts'
export type * from './models/AppInstall.ts'
export type * from './models/AutoAuth.ts'
export type * from './models/Device.ts'
export type * from './models/KVStore.ts'
export type * from './commonInputTypes.ts'

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

View File

@ -6,11 +6,10 @@
* 2. deviceInfoMiddleware - 仅获取设备信息不创建新设备 * 2. deviceInfoMiddleware - 仅获取设备信息不创建新设备
* 3. passwordMiddleware - 验证设备密码 * 3. passwordMiddleware - 验证设备密码
*/ */
import {prisma} from "../utils/prisma.js";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";
import {verifyDevicePassword} from "../utils/crypto.js"; import {verifyDevicePassword} from "../utils/crypto.js";
import {analyzeDevice} from "../utils/deviceDetector.js"; import {analyzeDevice} from "../utils/deviceDetector.js";
import { prisma } from "../utils/prisma.js";
/** /**
* 为新设备创建默认的自动登录配置 * 为新设备创建默认的自动登录配置

View File

@ -8,8 +8,8 @@
import {generateAccessToken, validateAccountToken, verifyAccessToken} from "../utils/tokenManager.js"; import {generateAccessToken, validateAccountToken, verifyAccessToken} from "../utils/tokenManager.js";
import {verifyToken} from "../utils/jwt.js"; import {verifyToken} from "../utils/jwt.js";
import {prisma} from "../utils/prisma.js";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";
import { prisma } from "../utils/prisma.js";
/** /**
* 新的JWT认证中间件支持refresh token系统 * 新的JWT认证中间件支持refresh token系统

View File

@ -4,9 +4,8 @@
* 仅验证app token设置设备和应用信息到res.locals * 仅验证app token设置设备和应用信息到res.locals
* 适用于所有KV相关的接口 * 适用于所有KV相关的接口
*/ */
import {prisma} from "../utils/prisma.js";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";
import { prisma } from "../utils/prisma.js";
/** /**
* KV Token认证中间件 * KV Token认证中间件

View File

@ -5,11 +5,10 @@
* 2. 验证密码或账户JWT二选一 * 2. 验证密码或账户JWT二选一
* 3. 适用于需要设备上下文的接口 * 3. 适用于需要设备上下文的接口
*/ */
import {prisma} from "../utils/prisma.js";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";
import {verifyToken as verifyAccountJWT} from "../utils/jwt.js"; import {verifyToken as verifyAccountJWT} from "../utils/jwt.js";
import {verifyDevicePassword} from "../utils/crypto.js"; import {verifyDevicePassword} from "../utils/crypto.js";
import { prisma } from "../utils/prisma.js";
/** /**
* UUID+密码/JWT混合认证中间件 * UUID+密码/JWT混合认证中间件

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "ClassworksKV", "name": "ClassworksKV",
"version": "1.3.10", "version": "1.3.15",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ClassworksKV", "name": "ClassworksKV",
"version": "1.3.10", "version": "1.3.15",
"dependencies": { "dependencies": {
"@opentelemetry/auto-instrumentations-node": "^0.59.0", "@opentelemetry/auto-instrumentations-node": "^0.59.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.205.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.205.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "ClassworksKV", "name": "ClassworksKV",
"version": "1.3.10", "version": "1.3.15",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node ./bin/www", "start": "node ./bin/www",
@ -32,12 +32,13 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"morgan": "~1.10.0", "morgan": "~1.10.0",
"node-device-detector": "^2.2.4", "node-device-detector": "^2.2.4",
"pg": "^8.18.0",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"pg": "^8.18.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"dotenv": "^16.5.0",
"prisma": "^7.3.0" "prisma": "^7.3.0"
} }
} }

View File

@ -2,109 +2,109 @@
CREATE SCHEMA IF NOT EXISTS "public"; CREATE SCHEMA IF NOT EXISTS "public";
-- CreateTable -- CreateTable
CREATE TABLE "account" ( CREATE TABLE "Account" (
"id" VARCHAR(191) NOT NULL, "id" VARCHAR(191) NOT NULL,
"provider" VARCHAR(191) NOT NULL, "provider" VARCHAR(191) NOT NULL,
"providerid" VARCHAR(191) NOT NULL, "providerId" VARCHAR(191) NOT NULL,
"email" VARCHAR(191), "email" VARCHAR(191),
"name" VARCHAR(191), "name" VARCHAR(191),
"avatarurl" VARCHAR(191), "avatarUrl" VARCHAR(191),
"providerdata" JSON, "providerData" JSON,
"accesstoken" TEXT, "accessToken" TEXT,
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedat" TIMESTAMPTZ(6) NOT NULL, "updatedAt" TIMESTAMPTZ(6) NOT NULL,
"refreshtoken" TEXT, "refreshToken" TEXT,
"refreshtokenexpiry" TIMESTAMPTZ(6), "refreshTokenExpiry" TIMESTAMPTZ(6),
"tokenversion" INTEGER NOT NULL DEFAULT 1, "tokenVersion" INTEGER NOT NULL DEFAULT 1,
CONSTRAINT "idx_18048_primary" PRIMARY KEY ("id") CONSTRAINT "idx_18303_PRIMARY" PRIMARY KEY ("id")
); );
-- CreateTable -- CreateTable
CREATE TABLE "appinstall" ( CREATE TABLE "AppInstall" (
"id" VARCHAR(191) NOT NULL, "id" VARCHAR(191) NOT NULL,
"deviceid" INTEGER NOT NULL, "deviceId" INTEGER NOT NULL,
"appid" VARCHAR(191) NOT NULL, "appId" VARCHAR(191) NOT NULL,
"token" VARCHAR(191) NOT NULL, "token" VARCHAR(191) NOT NULL,
"note" VARCHAR(191), "note" VARCHAR(191),
"installedat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, "installedAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedat" TIMESTAMPTZ(6) NOT NULL, "updatedAt" TIMESTAMPTZ(6) NOT NULL,
"devicetype" VARCHAR(191), "deviceType" VARCHAR(191),
"isreadonly" BOOLEAN NOT NULL DEFAULT false, "isReadOnly" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "idx_18055_primary" PRIMARY KEY ("id") CONSTRAINT "idx_18310_PRIMARY" PRIMARY KEY ("id")
); );
-- CreateTable -- CreateTable
CREATE TABLE "autoauth" ( CREATE TABLE "AutoAuth" (
"id" VARCHAR(191) NOT NULL, "id" VARCHAR(191) NOT NULL,
"deviceid" INTEGER NOT NULL, "deviceId" INTEGER NOT NULL,
"password" VARCHAR(191), "password" VARCHAR(191),
"devicetype" VARCHAR(191), "deviceType" VARCHAR(191),
"isreadonly" BOOLEAN NOT NULL DEFAULT false, "isReadOnly" BOOLEAN NOT NULL DEFAULT false,
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedat" TIMESTAMPTZ(6) NOT NULL, "updatedAt" TIMESTAMPTZ(6) NOT NULL,
CONSTRAINT "idx_18062_primary" PRIMARY KEY ("id") CONSTRAINT "idx_18317_PRIMARY" PRIMARY KEY ("id")
); );
-- CreateTable -- CreateTable
CREATE TABLE "device" ( CREATE TABLE "Device" (
"id" INTEGER NOT NULL, "id" INTEGER NOT NULL,
"uuid" VARCHAR(191) NOT NULL, "uuid" VARCHAR(191) NOT NULL,
"name" VARCHAR(191), "name" VARCHAR(191),
"accountid" VARCHAR(191), "accountId" VARCHAR(191),
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedat" TIMESTAMPTZ(6) NOT NULL, "updatedAt" TIMESTAMPTZ(6) NOT NULL,
"password" VARCHAR(191), "password" VARCHAR(191),
"passwordhint" VARCHAR(191), "passwordHint" VARCHAR(191),
"namespace" VARCHAR(191), "namespace" VARCHAR(191),
CONSTRAINT "idx_18069_primary" PRIMARY KEY ("id") CONSTRAINT "idx_18324_PRIMARY" PRIMARY KEY ("id")
); );
-- CreateTable -- CreateTable
CREATE TABLE "kvstore" ( CREATE TABLE "KVStore" (
"deviceid" INTEGER NOT NULL, "deviceId" INTEGER NOT NULL,
"key" VARCHAR(191) NOT NULL, "key" VARCHAR(191) NOT NULL,
"value" JSON NOT NULL, "value" JSON NOT NULL,
"creatorip" VARCHAR(191) DEFAULT '', "creatorIp" VARCHAR(191) DEFAULT '',
"createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedat" TIMESTAMPTZ(6) NOT NULL, "updatedAt" TIMESTAMPTZ(6) NOT NULL,
CONSTRAINT "idx_18075_primary" PRIMARY KEY ("deviceid","key") CONSTRAINT "idx_18330_PRIMARY" PRIMARY KEY ("deviceId","key")
); );
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "idx_18048_account_provider_providerid_key" ON "account"("provider", "providerid"); CREATE UNIQUE INDEX "idx_18303_Account_provider_providerId_key" ON "Account"("provider", "providerId");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "idx_18055_appinstall_token_key" ON "appinstall"("token"); CREATE UNIQUE INDEX "idx_18310_AppInstall_token_key" ON "AppInstall"("token");
-- CreateIndex -- CreateIndex
CREATE INDEX "idx_18055_appinstall_deviceid_fkey" ON "appinstall"("deviceid"); CREATE INDEX "idx_18310_AppInstall_deviceId_fkey" ON "AppInstall"("deviceId");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "idx_18062_autoauth_deviceid_password_key" ON "autoauth"("deviceid", "password"); CREATE UNIQUE INDEX "idx_18317_AutoAuth_deviceId_password_key" ON "AutoAuth"("deviceId", "password");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "idx_18069_device_uuid_key" ON "device"("uuid"); CREATE UNIQUE INDEX "idx_18324_Device_uuid_key" ON "Device"("uuid");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "idx_18069_device_namespace_key" ON "device"("namespace"); CREATE UNIQUE INDEX "idx_18324_Device_namespace_key" ON "Device"("namespace");
-- CreateIndex -- CreateIndex
CREATE INDEX "idx_18069_device_accountid_fkey" ON "device"("accountid"); CREATE INDEX "idx_18324_Device_accountId_fkey" ON "Device"("accountId");
-- AddForeignKey -- AddForeignKey
ALTER TABLE "appinstall" ADD CONSTRAINT "appinstall_deviceid_fkey" FOREIGN KEY ("deviceid") REFERENCES "device"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "AppInstall" ADD CONSTRAINT "AppInstall_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "autoauth" ADD CONSTRAINT "autoauth_deviceid_fkey" FOREIGN KEY ("deviceid") REFERENCES "device"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "AutoAuth" ADD CONSTRAINT "AutoAuth_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "device" ADD CONSTRAINT "device_accountid_fkey" FOREIGN KEY ("accountid") REFERENCES "account"("id") ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE "Device" ADD CONSTRAINT "Device_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "kvstore" ADD CONSTRAINT "kvstore_deviceid_fkey" FOREIGN KEY ("deviceid") REFERENCES "device"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "KVStore" ADD CONSTRAINT "KVStore_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "Device"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,14 @@
-- AlterTable
ALTER TABLE "Account" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "AppInstall" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "AutoAuth" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "Device" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "KVStore" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;

View File

@ -0,0 +1,4 @@
-- AlterTable
CREATE SEQUENCE device_id_seq;
ALTER TABLE "Device" ALTER COLUMN "id" SET DEFAULT nextval('device_id_seq');
ALTER SEQUENCE device_id_seq OWNED BY "Device"."id";

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -7,79 +7,84 @@ datasource db {
provider = "postgresql" provider = "postgresql"
} }
model account { model Account {
id String @id(map: "idx_18048_primary") @db.VarChar(191) id String @id(map: "idx_18303_PRIMARY") @db.VarChar(191) @default(cuid())
provider String @db.VarChar(191) provider String @db.VarChar(191)
providerid String @db.VarChar(191) providerId String @db.VarChar(191)
email String? @db.VarChar(191) email String? @db.VarChar(191)
name String? @db.VarChar(191) name String? @db.VarChar(191)
avatarurl String? @db.VarChar(191) avatarUrl String? @db.VarChar(191)
providerdata Json? @db.Json providerData Json? @db.Json
accesstoken String? accessToken String?
createdat DateTime @default(now()) @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedat DateTime @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
refreshtoken String? refreshToken String?
refreshtokenexpiry DateTime? @db.Timestamptz(6) refreshTokenExpiry DateTime? @db.Timestamptz(6)
tokenversion Int @default(1) tokenVersion Int @default(1)
device device[]
@@unique([provider, providerid], map: "idx_18048_account_provider_providerid_key") devices Device[]
@@unique([provider, providerId], map: "idx_18303_Account_provider_providerId_key")
} }
model appinstall { model AppInstall {
id String @id(map: "idx_18055_primary") @db.VarChar(191) id String @id(map: "idx_18310_PRIMARY") @default(cuid()) @db.VarChar(191)
deviceid Int deviceId Int
appid String @db.VarChar(191) appId String @db.VarChar(191)
token String @unique(map: "idx_18055_appinstall_token_key") @db.VarChar(191) token String @unique(map: "idx_18310_AppInstall_token_key") @db.VarChar(191)
note String? @db.VarChar(191) note String? @db.VarChar(191)
installedat DateTime @default(now()) @db.Timestamptz(6) installedAt DateTime @default(now()) @db.Timestamptz(6)
updatedat DateTime @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
devicetype String? @db.VarChar(191) deviceType String? @db.VarChar(191)
isreadonly Boolean @default(false) isReadOnly Boolean @default(false)
device device @relation(fields: [deviceid], references: [id], onDelete: Cascade)
@@index([deviceid], map: "idx_18055_appinstall_deviceid_fkey") device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
@@index([deviceId], map: "idx_18310_AppInstall_deviceId_fkey")
} }
model autoauth { model AutoAuth {
id String @id(map: "idx_18062_primary") @db.VarChar(191) id String @id(map: "idx_18317_PRIMARY") @default(cuid()) @db.VarChar(191)
deviceid Int deviceId Int
password String? @db.VarChar(191) password String? @db.VarChar(191)
devicetype String? @db.VarChar(191) deviceType String? @db.VarChar(191)
isreadonly Boolean @default(false) isReadOnly Boolean @default(false)
createdat DateTime @default(now()) @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedat DateTime @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
device device @relation(fields: [deviceid], references: [id], onDelete: Cascade)
@@unique([deviceid, password], map: "idx_18062_autoauth_deviceid_password_key") device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
@@unique([deviceId, password], map: "idx_18317_AutoAuth_deviceId_password_key")
} }
model device { model Device {
id Int @id(map: "idx_18069_primary") id Int @id(map: "idx_18324_PRIMARY") @default(autoincrement())
uuid String @unique(map: "idx_18069_device_uuid_key") @db.VarChar(191) uuid String @unique(map: "idx_18324_Device_uuid_key") @db.VarChar(191)
name String? @db.VarChar(191) name String? @db.VarChar(191)
accountid String? @db.VarChar(191) accountId String? @db.VarChar(191)
createdat DateTime @default(now()) @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedat DateTime @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
password String? @db.VarChar(191) password String? @db.VarChar(191)
passwordhint String? @db.VarChar(191) passwordHint String? @db.VarChar(191)
namespace String? @unique(map: "idx_18069_device_namespace_key") @db.VarChar(191) namespace String? @unique(map: "idx_18324_Device_namespace_key") @db.VarChar(191)
appinstall appinstall[]
autoauth autoauth[]
account account? @relation(fields: [accountid], references: [id])
kvstore kvstore[]
@@index([accountid], map: "idx_18069_device_accountid_fkey") // 关联关系
account Account? @relation(fields: [accountId], references: [id], onDelete: SetNull)
appInstalls AppInstall[]
kvStore KVStore[] // 设备相关的KV存储
autoAuths AutoAuth[] // 自动授权配置
@@index([accountId], map: "idx_18324_Device_accountId_fkey")
} }
model kvstore { model KVStore {
deviceid Int deviceId Int
key String @db.VarChar(191) key String @db.VarChar(191)
value Json @db.Json value Json @db.Json
creatorip String? @default("") @db.VarChar(191) creatorIp String? @default("") @db.VarChar(191)
createdat DateTime @default(now()) @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedat DateTime @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(6)
device device @relation(fields: [deviceid], references: [id], onDelete: Cascade)
@@id([deviceid, key], map: "idx_18075_primary") device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
@@id([deviceId, key], map: "idx_18330_PRIMARY")
} }

View File

@ -1,10 +1,10 @@
import {Router} from "express"; import {Router} from "express";
import {prisma} from "../utils/prisma.js";
import crypto from "crypto"; import crypto from "crypto";
import {generateState, getCallbackURL, oauthProviders} from "../config/oauth.js"; import {generateState, getCallbackURL, oauthProviders} from "../config/oauth.js";
import {generateTokenPair, refreshAccessToken, revokeAllTokens, revokeRefreshToken} from "../utils/jwt.js"; import {generateTokenPair, refreshAccessToken, revokeAllTokens, revokeRefreshToken} from "../utils/jwt.js";
import {jwtAuth} from "../middleware/jwt-auth.js"; import {jwtAuth} from "../middleware/jwt-auth.js";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";
import { prisma } from "../utils/prisma.js";
const router = Router(); const router = Router();

View File

@ -1,9 +1,9 @@
import {Router} from "express"; import {Router} from "express";
import {uuidAuth} from "../middleware/uuidAuth.js"; import {uuidAuth} from "../middleware/uuidAuth.js";
import {prisma} from "../utils/prisma.js";
import crypto from "crypto"; import crypto from "crypto";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";
import {verifyDevicePassword} from "../utils/crypto.js"; import {verifyDevicePassword} from "../utils/crypto.js";
import { prisma } from "../utils/prisma.js";
const router = Router(); const router = Router();
@ -289,7 +289,7 @@ router.post(
} }
// 读取设备的 classworks-list-main 键值 // 读取设备的 classworks-list-main 键值
const kvRecord = await prisma.kvstore.findUnique({ const kvRecord = await prisma.kVStore.findUnique({
where: { where: {
deviceId_key: { deviceId_key: {
deviceId: appInstall.deviceId, deviceId: appInstall.deviceId,

View File

@ -1,7 +1,7 @@
import {Router} from "express"; import {Router} from "express";
import {jwtAuth} from "../middleware/jwt-auth.js"; import {jwtAuth} from "../middleware/jwt-auth.js";
import {prisma} from "../utils/prisma.js";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";
import { prisma } from "../utils/prisma.js";
const router = Router(); const router = Router();

View File

@ -1,7 +1,7 @@
import {Router} from "express"; import {Router} from "express";
import deviceCodeStore from "../utils/deviceCodeStore.js"; import deviceCodeStore from "../utils/deviceCodeStore.js";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";
import {prisma} from "../utils/prisma.js"; import { prisma } from "../utils/prisma.js";
const router = Router(); const router = Router();

View File

@ -1,9 +1,9 @@
import {Router} from "express"; import {Router} from "express";
import {extractDeviceInfo} from "../middleware/uuidAuth.js"; import {extractDeviceInfo} from "../middleware/uuidAuth.js";
import {prisma} from "../utils/prisma.js";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";
import {getOnlineDevices} from "../utils/socket.js"; import {getOnlineDevices} from "../utils/socket.js";
import {registeredDevicesTotal} from "../utils/metrics.js"; import {registeredDevicesTotal} from "../utils/metrics.js";
import { prisma } from "../utils/prisma.js";
const router = Router(); const router = Router();

View File

@ -1,5 +1,5 @@
import {prisma} from "./prisma.js";
import {keysTotal} from "./metrics.js"; import {keysTotal} from "./metrics.js";
import { prisma } from "./prisma.js";
class KVStore { class KVStore {
/** /**
@ -9,7 +9,7 @@ class KVStore {
* @returns {object|null} 键对应的值或null * @returns {object|null} 键对应的值或null
*/ */
async get(deviceId, key) { async get(deviceId, key) {
const item = await prisma.kvstore.findUnique({ const item = await prisma.kVStore.findUnique({
where: { where: {
deviceId_key: { deviceId_key: {
deviceId: deviceId, deviceId: deviceId,
@ -27,7 +27,7 @@ class KVStore {
* @returns {object|null} 键的完整信息或null * @returns {object|null} 键的完整信息或null
*/ */
async getMetadata(deviceId, key) { async getMetadata(deviceId, key) {
const item = await prisma.kvstore.findUnique({ const item = await prisma.kVStore.findUnique({
where: { where: {
deviceId_key: { deviceId_key: {
deviceId: deviceId, deviceId: deviceId,
@ -66,7 +66,7 @@ class KVStore {
* @returns {object} 创建或更新的记录 * @returns {object} 创建或更新的记录
*/ */
async upsert(deviceId, key, value, creatorIp = "") { async upsert(deviceId, key, value, creatorIp = "") {
const item = await prisma.kvstore.upsert({ const item = await prisma.kVStore.upsert({
where: { where: {
deviceId_key: { deviceId_key: {
deviceId: deviceId, deviceId: deviceId,
@ -86,7 +86,7 @@ class KVStore {
}); });
// 更新键总数指标 // 更新键总数指标
const totalKeys = await prisma.kvstore.count(); const totalKeys = await prisma.kVStore.count();
keysTotal.set(totalKeys); keysTotal.set(totalKeys);
// 返回带有设备ID和原始键的结果 // 返回带有设备ID和原始键的结果
@ -115,7 +115,7 @@ class KVStore {
await prisma.$transaction(async (tx) => { await prisma.$transaction(async (tx) => {
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
try { try {
const item = await tx.kvstore.upsert({ const item = await tx.kVStore.upsert({
where: { where: {
deviceId_key: { deviceId_key: {
deviceId: deviceId, deviceId: deviceId,
@ -150,7 +150,7 @@ class KVStore {
}); });
// 在事务完成后,一次性更新指标 // 在事务完成后,一次性更新指标
const totalKeys = await prisma.kvstore.count(); const totalKeys = await prisma.kVStore.count();
keysTotal.set(totalKeys); keysTotal.set(totalKeys);
return { results, errors }; return { results, errors };
@ -164,7 +164,7 @@ class KVStore {
*/ */
async delete(deviceId, key) { async delete(deviceId, key) {
try { try {
const item = await prisma.kvstore.delete({ const item = await prisma.kVStore.delete({
where: { where: {
deviceId_key: { deviceId_key: {
deviceId: deviceId, deviceId: deviceId,
@ -174,7 +174,7 @@ class KVStore {
}); });
// 更新键总数指标 // 更新键总数指标
const totalKeys = await prisma.kvstore.count(); const totalKeys = await prisma.kVStore.count();
keysTotal.set(totalKeys); keysTotal.set(totalKeys);
return item ? {...item, deviceId, key} : null; return item ? {...item, deviceId, key} : null;
@ -201,7 +201,7 @@ class KVStore {
orderBy[sortBy] = sortDir.toLowerCase(); orderBy[sortBy] = sortDir.toLowerCase();
// 查询设备的所有键 // 查询设备的所有键
const items = await prisma.kvstore.findMany({ const items = await prisma.kVStore.findMany({
where: { where: {
deviceId: deviceId, deviceId: deviceId,
}, },
@ -244,7 +244,7 @@ class KVStore {
orderBy[sortBy] = sortDir.toLowerCase(); orderBy[sortBy] = sortDir.toLowerCase();
// 查询设备的所有键,只选择键名 // 查询设备的所有键,只选择键名
const items = await prisma.kvstore.findMany({ const items = await prisma.kVStore.findMany({
where: { where: {
deviceId: deviceId, deviceId: deviceId,
}, },
@ -266,7 +266,7 @@ class KVStore {
* @returns {number} 键值对数量 * @returns {number} 键值对数量
*/ */
async count(deviceId) { async count(deviceId) {
const count = await prisma.kvstore.count({ const count = await prisma.kVStore.count({
where: { where: {
deviceId: deviceId, deviceId: deviceId,
}, },
@ -281,15 +281,15 @@ class KVStore {
*/ */
async getStats(deviceId) { async getStats(deviceId) {
const [totalKeys, oldestKey, newestKey] = await Promise.all([ const [totalKeys, oldestKey, newestKey] = await Promise.all([
prisma.kvstore.count({ prisma.kVStore.count({
where: { deviceId }, where: { deviceId },
}), }),
prisma.kvstore.findFirst({ prisma.kVStore.findFirst({
where: { deviceId }, where: { deviceId },
orderBy: { createdAt: "asc" }, orderBy: { createdAt: "asc" },
select: { createdAt: true, key: true }, select: { createdAt: true, key: true },
}), }),
prisma.kvstore.findFirst({ prisma.kVStore.findFirst({
where: { deviceId }, where: { deviceId },
orderBy: { updatedAt: "desc" }, orderBy: { updatedAt: "desc" },
select: { updatedAt: true, key: true }, select: { updatedAt: true, key: true },

View File

@ -33,9 +33,8 @@ export async function initializeMetrics() {
registeredDevicesTotal.set(deviceCount); registeredDevicesTotal.set(deviceCount);
// 获取已创建键总数 // 获取已创建键总数
const keyCount = await prisma.kvstore.count(); const keyCount = await prisma.kVStore.count();
keysTotal.set(keyCount); keysTotal.set(keyCount);
console.log('Prometheus metrics initialized - Devices:', deviceCount, 'Keys:', keyCount); console.log('Prometheus metrics initialized - Devices:', deviceCount, 'Keys:', keyCount);
} catch (error) { } catch (error) {
console.error('Failed to initialize metrics:', error); console.error('Failed to initialize metrics:', error);

View File

@ -1,5 +1,5 @@
import {prisma} from "./prisma.js";
import kvStore from "./kvStore.js"; import kvStore from "./kvStore.js";
import { prisma } from "./prisma.js";
// 系统保留UUID用于存储站点信息 // 系统保留UUID用于存储站点信息
const SYSTEM_DEVICE_UUID = "00000000-0000-4000-8000-000000000000"; const SYSTEM_DEVICE_UUID = "00000000-0000-4000-8000-000000000000";

View File

@ -13,10 +13,10 @@
*/ */
import { Server } from "socket.io"; import { Server } from "socket.io";
import { prisma } from "./prisma.js";
import { onlineDevicesGauge } from "./metrics.js"; import { onlineDevicesGauge } from "./metrics.js";
import DeviceDetector from "node-device-detector"; import DeviceDetector from "node-device-detector";
import ClientHints from "node-device-detector/client-hints.js"; import ClientHints from "node-device-detector/client-hints.js";
import { prisma } from "./prisma.js";
// Socket.IO 单例实例 // Socket.IO 单例实例
let io = null; let io = null;

View File

@ -1,6 +1,6 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import crypto from 'crypto'; import crypto from 'crypto';
import {prisma} from './prisma.js'; import { prisma } from './prisma.js';
// Token 配置 // Token 配置
const ACCESS_TOKEN_SECRET = process.env.JWT_SECRET || 'your-access-token-secret-change-this-in-production'; const ACCESS_TOKEN_SECRET = process.env.JWT_SECRET || 'your-access-token-secret-change-this-in-production';