diff --git a/.gitignore b/.gitignore index d23defa..df5de01 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,7 @@ dist prisma/database/data data/ + +/generated/prisma + +/generated/prisma diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 35410ca..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/FixClassworksKV.iml b/.idea/FixClassworksKV.iml deleted file mode 100644 index 24643cc..0000000 --- a/.idea/FixClassworksKV.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 09fff6f..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/API_AUTOAUTH.md b/API_AUTOAUTH.md deleted file mode 100644 index 7b73c17..0000000 --- a/API_AUTOAUTH.md +++ /dev/null @@ -1,276 +0,0 @@ -# AutoAuth 和新增 Apps API 文档 - -## 概述 - -本文档描述了自动授权 (AutoAuth) 相关的 API 接口以及新增的应用管理接口。 - ---- - -## Apps API 新增接口 - -### 1. 通过 namespace 和密码获取 token - -**端点**: `POST /apps/auth/token` - -**描述**: 通过设备的 namespace 和密码进行自动授权,创建新的 AppInstall 并返回 token。 - -**请求体**: -```json -{ - "namespace": "string (必填)", - "password": "string (可选,根据自动授权配置)", - "appId": "string (必填)" -} -``` - -**成功响应** (201 Created): -```json -{ - "success": true, - "token": "string", - "deviceType": "string | null", - "isReadOnly": boolean, - "installedAt": "datetime" -} -``` - -**错误响应**: -- `400 Bad Request`: 缺少必填字段 -- `404 Not Found`: 设备不存在或 namespace 不正确 -- `401 Unauthorized`: 密码不正确或需要提供密码 - -**说明**: -- 该接口会查找匹配的 AutoAuth 配置 -- 如果提供了密码,会验证密码是否匹配任何自动授权配置 -- 如果没有提供密码,会查找无密码的自动授权配置 -- 根据匹配的 AutoAuth 配置设置 `deviceType` 和 `isReadOnly` 属性 - ---- - -### 2. 设置学生名称 - -**端点**: `POST /apps/tokens/:token/set-student-name` - -**描述**: 为学生类型的 token 设置名称(更新 note 字段)。 - -**URL 参数**: -- `token`: AppInstall 的 token - -**请求体**: -```json -{ - "name": "string (必填)" -} -``` - -**成功响应** (200 OK): -```json -{ - "success": true, - "token": "string", - "name": "string", - "updatedAt": "datetime" -} -``` - -**错误响应**: -- `400 Bad Request`: 缺少名称或名称不在学生列表中 -- `403 Forbidden`: token 类型不是 student -- `404 Not Found`: token 不存在或设备未设置学生列表 - -**说明**: -- 只有 `deviceType` 为 `student` 的 token 才能使用此接口 -- 会验证提供的名称是否存在于设备的 `classworks-list-main` 键值中 -- 学生列表格式: `[{"id": 1, "name": "学生1"}, {"id": 2, "name": "学生2"}]` - ---- - -## AutoAuth 管理 API - -> 🔐 **所有 AutoAuth 管理接口都需要 JWT Account Token 认证** -> -> **重要**: 只有已绑定账户的设备才能使用这些接口。未绑定账户的设备无法管理 AutoAuth 配置。 -> -> 通过 HTTP Headers 提供: -> - `Authorization`: `Bearer {jwt_token}` - 账户的 JWT Token - -### 1. 获取设备的自动授权配置列表 - -**端点**: `GET /auto-auth/devices/:uuid/auth-configs` - -**认证**: 需要 JWT Token (账户必须是设备的拥有者) - -**URL 参数**: -- `uuid`: 设备的 UUID - -**成功响应** (200 OK): -```json -{ - "success": true, - "configs": [ - { - "id": "string", - "hasPassword": boolean, - "deviceType": "string | null", - "isReadOnly": boolean, - "createdAt": "datetime", - "updatedAt": "datetime" - } - ] -} -``` - -**说明**: -- 返回的配置不包含实际的密码哈希值,只显示是否有密码 - ---- - -### 2. 创建自动授权配置 - -**端点**: `POST /auto-auth/devices/:uuid/auth-configs` - -**认证**: 需要 JWT Token (账户必须是设备的拥有者) - -**URL 参数**: -- `uuid`: 设备的 UUID - -**请求体**: -```json -{ - "password": "string (可选)", - "deviceType": "string (可选: teacher|student|classroom|parent)", - "isReadOnly": boolean (可选,默认 false) -} -``` - -**成功响应** (201 Created): -```json -{ - "success": true, - "config": { - "id": "string", - "hasPassword": boolean, - "deviceType": "string | null", - "isReadOnly": boolean, - "createdAt": "datetime" - } -} -``` - -**错误响应**: -- `400 Bad Request`: 设备类型无效或密码配置已存在 - -**说明**: -- 同一设备的密码必须唯一(包括空密码) -- `deviceType` 必须是 `teacher`、`student`、`classroom`、`parent` 之一,或为空 - ---- - -### 3. 更新自动授权配置 - -**端点**: `PUT /auto-auth/devices/:uuid/auth-configs/:configId` - -**认证**: 需要 JWT Token (账户必须是设备的拥有者) - -**URL 参数**: -- `uuid`: 设备的 UUID -- `configId`: 自动授权配置的 ID - -**请求体**: -```json -{ - "password": "string (可选)", - "deviceType": "string (可选: teacher|student|classroom|parent)", - "isReadOnly": boolean (可选) -} -``` - -**成功响应** (200 OK): -```json -{ - "success": true, - "config": { - "id": "string", - "hasPassword": boolean, - "deviceType": "string | null", - "isReadOnly": boolean, - "updatedAt": "datetime" - } -} -``` - -**错误响应**: -- `400 Bad Request`: 设备类型无效或新密码与其他配置冲突 -- `403 Forbidden`: 无权操作此配置 -- `404 Not Found`: 配置不存在 - -**说明**: -- 只能更新属于当前设备的配置 -- 更新密码时会检查是否与该设备的其他配置冲突 - ---- - -### 4. 删除自动授权配置 - -**端点**: `DELETE /auto-auth/devices/:uuid/auth-configs/:configId` - -**认证**: 需要 JWT Token (账户必须是设备的拥有者) - -**URL 参数**: -- `uuid`: 设备的 UUID -- `configId`: 自动授权配置的 ID - -**成功响应** (204 No Content): -- 无响应体 - -**错误响应**: -- `403 Forbidden`: 无权操作此配置 -- `404 Not Found`: 配置不存在 - -**说明**: -- 只能删除属于当前设备的配置 - ---- - -## 设备类型 (deviceType) - -可选的设备类型值: -- `teacher`: 教师 -- `student`: 学生 -- `classroom`: 班级一体机 -- `parent`: 家长 -- `null`: 未指定类型 - ---- - -## 使用流程示例 - -### 场景 1: 学生使用 namespace 登录 - -1. 学生输入班级的 namespace 和密码 -2. 调用 `POST /apps/auth/token` 获取 token -3. 使用返回的 token 访问 KV 存储 -4. 如果是学生类型,调用 `POST /apps/tokens/:token/set-student-name` 设置自己的名称 - -### 场景 2: 管理员配置自动授权 - -1. 管理员通过账户登录获取 JWT Token -2. 调用 `POST /auto-auth/devices/:uuid/auth-configs` 创建多个授权配置: - - 教师密码(deviceType: teacher, isReadOnly: false) - - 学生密码(deviceType: student, isReadOnly: false) - - 家长密码(deviceType: parent, isReadOnly: true) -3. 学生/教师/家长使用对应密码通过 namespace 登录 - -**注意**: 设备必须已绑定到管理员的账户才能配置 AutoAuth - ---- - -## 注意事项 - -1. **密码安全**: 所有密码都使用 bcrypt 进行哈希存储 -2. **唯一性约束**: - - 同一设备的 namespace 必须唯一 - - 同一设备的 AutoAuth 密码必须唯一(包括 null) -3. **级联删除**: 删除设备会级联删除所有相关的 AutoAuth 配置和 AppInstall 记录 -4. **只读限制**: isReadOnly 为 true 的 token 在 KV 操作中会受到写入限制 -5. **账户绑定要求**: 只有已绑定账户的设备才能管理 AutoAuth 配置,未绑定账户的设备无法使用 AutoAuth 管理接口 diff --git a/API_QUICK_REFERENCE.md b/API_QUICK_REFERENCE.md deleted file mode 100644 index e69de29..0000000 diff --git a/Dockerfile b/Dockerfile index 05be299..f17a5a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine +FROM node:24-alpine WORKDIR /app diff --git a/MIGRATION_CHECKLIST.md b/MIGRATION_CHECKLIST.md deleted file mode 100644 index 7fa6971..0000000 --- a/MIGRATION_CHECKLIST.md +++ /dev/null @@ -1,129 +0,0 @@ -# Refresh Token系统迁移检查清单 - -## 🔧 服务端迁移 - -### 数据库 -- [ ] 运行Prisma迁移: `npx prisma migrate dev --name add_refresh_token_system` -- [ ] 验证Account表新增字段: refreshToken, refreshTokenExpiry, tokenVersion - -### 环境配置 -- [ ] 添加 `ACCESS_TOKEN_EXPIRES_IN=15m` -- [ ] 添加 `REFRESH_TOKEN_EXPIRES_IN=7d` -- [ ] 添加 `REFRESH_TOKEN_SECRET=your-refresh-token-secret` -- [ ] (可选)配置RSA密钥对 - -### 代码验证 -- [ ] `utils/tokenManager.js` 文件已创建 -- [ ] `utils/jwt.js` 已更新(保持向后兼容) -- [ ] `middleware/jwt-auth.js` 已升级 -- [ ] `routes/accounts.js` 新增refresh相关端点 - -## 🖥️ 前端迁移 - -### OAuth回调处理 -- [ ] 更新回调URL参数解析(支持access_token和refresh_token) -- [ ] 保持对旧版token参数的兼容性 -- [ ] 实现TokenManager类 - -### Token管理 -- [ ] 实现Token刷新逻辑 -- [ ] 添加请求拦截器检查X-New-Access-Token响应头 -- [ ] 实现401错误自动重试机制 -- [ ] 添加登出功能(单设备/全设备) - -### 存储策略 -- [ ] Access Token存储(localStorage/sessionStorage) -- [ ] Refresh Token安全存储 -- [ ] 实现Token清理逻辑 - -## 🧪 测试验证 - -### 功能测试 -- [ ] OAuth登录流程测试 -- [ ] Token自动刷新测试 -- [ ] 手动refresh接口测试 -- [ ] 登出功能测试(单设备) -- [ ] 登出功能测试(全设备) -- [ ] Token信息查看测试 - -### 兼容性测试 -- [ ] 旧版JWT token仍然有效 -- [ ] 新旧token混合使用场景 -- [ ] API向后兼容性验证 - -### 错误处理测试 -- [ ] 过期token处理 -- [ ] 无效refresh token处理 -- [ ] 网络错误重试 -- [ ] 并发刷新场景 - -## 📊 监控配置 - -### 日志记录 -- [ ] Token生成日志 -- [ ] Token刷新日志 -- [ ] 认证失败日志 -- [ ] 登出操作日志 - -### 性能监控 -- [ ] Token刷新频率统计 -- [ ] API响应时间监控 -- [ ] 数据库查询性能 - -## 🔒 安全检查 - -### Token安全 -- [ ] 密钥强度验证 -- [ ] Token过期时间配置合理 -- [ ] HTTPS传输确认 -- [ ] 敏感信息不在日志中暴露 - -### 访问控制 -- [ ] Token撤销功能正常 -- [ ] 版本控制机制有效 -- [ ] 设备隔离正确 - -## 📚 文档检查 - -- [ ] API文档已更新 -- [ ] 前端集成指南已提供 -- [ ] 迁移步骤文档完整 -- [ ] 错误处理指南清晰 - -## 🚀 上线准备 - -### 部署前 -- [ ] 代码review完成 -- [ ] 单元测试通过 -- [ ] 集成测试通过 -- [ ] 性能测试通过 - -### 部署时 -- [ ] 数据库迁移执行 -- [ ] 环境变量配置 -- [ ] 服务重启验证 -- [ ] 健康检查通过 - -### 部署后 -- [ ] 新用户登录测试 -- [ ] 现有用户功能正常 -- [ ] 监控指标正常 -- [ ] 错误日志检查 - -## 🔄 回滚计划 - -### 紧急回滚 -- [ ] 回滚代码到上一版本 -- [ ] 恢复原环境变量 -- [ ] 数据库回滚方案(如需要) - -### 数据迁移回滚 -- [ ] 备份新增字段数据 -- [ ] 移除新增字段的迁移脚本 -- [ ] 验证旧版功能正常 - ---- - -**检查完成人员**: ___________ -**检查完成时间**: ___________ -**环境**: [ ] 开发 [ ] 测试 [ ] 生产 \ No newline at end of file diff --git a/NEW_APIS_SUMMARY.md b/NEW_APIS_SUMMARY.md deleted file mode 100644 index e69de29..0000000 diff --git a/REFRESH_TOKEN_API.md b/REFRESH_TOKEN_API.md deleted file mode 100644 index 44c1929..0000000 --- a/REFRESH_TOKEN_API.md +++ /dev/null @@ -1,489 +0,0 @@ -# Refresh Token系统API文档 - -## 概述 - -ClassworksKV现在支持标准的Refresh Token认证系统,提供更安全的用户认证机制。新系统包含: - -- **Access Token**: 短期令牌(默认15分钟),用于API访问 -- **Refresh Token**: 长期令牌(默认7天),用于刷新Access Token -- **Token版本控制**: 支持令牌失效和安全登出 -- **向后兼容**: 支持旧版JWT令牌 - -## 配置选项 - -可以通过环境变量配置token系统: - -```bash -# Access Token配置 -ACCESS_TOKEN_EXPIRES_IN=15m # Access Token过期时间 -REFRESH_TOKEN_EXPIRES_IN=7d # Refresh Token过期时间 - -# 密钥配置(HS256算法) -JWT_SECRET=your-access-token-secret -REFRESH_TOKEN_SECRET=your-refresh-token-secret - -# RSA密钥配置(RS256算法,可选) -JWT_ALG=RS256 -ACCESS_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..." -ACCESS_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n..." -REFRESH_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..." -REFRESH_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n..." -``` - -## API端点 - -### 1. OAuth登录回调 - -OAuth登录成功后,系统会返回令牌对。 - -**回调URL参数(新版):** -``` -https://your-frontend.com/?access_token=eyJ...&refresh_token=eyJ...&expires_in=15m&success=true&provider=github -``` - -**旧版兼容参数:** -``` -https://your-frontend.com/?token=eyJ...&success=true&provider=github -``` - -### 2. 刷新访问令牌 - -当Access Token即将过期或已过期时,使用Refresh Token获取新的Access Token。 - -**端点:** `POST /api/accounts/refresh` - -**请求体:** -```json -{ - "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." -} -``` - -**响应(成功):** -```json -{ - "success": true, - "message": "令牌刷新成功", - "data": { - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "expires_in": "15m", - "account": { - "id": "clxxxx", - "provider": "github", - "email": "user@example.com", - "name": "User Name", - "avatarUrl": "https://..." - } - } -} -``` - -**错误响应:** -```json -{ - "success": false, - "message": "刷新令牌已过期" -} -``` - -**错误状态码:** -- `400`: 缺少刷新令牌 -- `401`: 无效的刷新令牌、令牌已过期、账户不存在、令牌版本不匹配 - -### 3. 登出(当前设备) - -撤销当前设备的Refresh Token,但不影响其他设备。 - -**端点:** `POST /api/accounts/logout` - -**请求头:** -``` -Authorization: Bearer -``` - -**响应:** -```json -{ - "success": true, - "message": "登出成功" -} -``` - -### 4. 登出所有设备 - -撤销账户的所有令牌,强制所有设备重新登录。 - -**端点:** `POST /api/accounts/logout-all` - -**请求头:** -``` -Authorization: Bearer -``` - -**响应:** -```json -{ - "success": true, - "message": "已从所有设备登出" -} -``` - -### 5. 获取令牌信息 - -查看当前令牌的详细信息和状态。 - -**端点:** `GET /api/accounts/token-info` - -**请求头:** -``` -Authorization: Bearer -``` - -**响应:** -```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逻辑 \ No newline at end of file diff --git a/REFRESH_TOKEN_QUICKSTART.md b/REFRESH_TOKEN_QUICKSTART.md deleted file mode 100644 index 2f96cf4..0000000 --- a/REFRESH_TOKEN_QUICKSTART.md +++ /dev/null @@ -1,112 +0,0 @@ -# Refresh Token系统 - 快速使用指南 - -## 🚀 快速开始 - -### 1. 环境变量配置 - -```bash -# 添加到 .env 文件 -ACCESS_TOKEN_EXPIRES_IN=15m -REFRESH_TOKEN_EXPIRES_IN=7d -REFRESH_TOKEN_SECRET=your-refresh-token-secret-change-this -``` - -### 2. 数据库迁移 - -```bash -npx prisma migrate dev --name add_refresh_token_system -``` - -### 3. 新的OAuth回调参数 - -登录成功后,回调URL现在包含: -``` -?access_token=eyJ...&refresh_token=eyJ...&expires_in=15m&success=true -``` - -## 📝 核心API - -### 刷新Token -```http -POST /api/accounts/refresh -Content-Type: application/json - -{ - "refresh_token": "eyJ..." -} -``` - -### 登出当前设备 -```http -POST /api/accounts/logout -Authorization: Bearer -``` - -### 登出所有设备 -```http -POST /api/accounts/logout-all -Authorization: Bearer -``` - -## 💻 前端集成 - -### 基础Token管理 -```javascript -class TokenManager { - setTokens(accessToken, refreshToken) { - localStorage.setItem('access_token', accessToken); - localStorage.setItem('refresh_token', refreshToken); - } - - async refreshToken() { - const refreshToken = localStorage.getItem('refresh_token'); - const response = await fetch('/api/accounts/refresh', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refresh_token: refreshToken }) - }); - - const data = await response.json(); - if (data.success) { - localStorage.setItem('access_token', data.data.access_token); - return data.data.access_token; - } - throw new Error(data.message); - } -} -``` - -### 自动刷新拦截器 -```javascript -// 检查响应头中的新token -const newToken = response.headers.get('X-New-Access-Token'); -if (newToken) { - localStorage.setItem('access_token', newToken); -} - -// 401错误时自动刷新 -if (response.status === 401) { - await tokenManager.refreshToken(); - // 重试请求 -} -``` - -## 🔒 安全特性 - -- ✅ 短期Access Token(15分钟) -- ✅ 长期Refresh Token(7天) -- ✅ Token版本控制 -- ✅ 设备级登出 -- ✅ 全局登出 -- ✅ 自动刷新机制 -- ✅ 向后兼容 - -## 🔄 迁移步骤 - -1. **更新环境变量** -2. **运行数据库迁移** -3. **更新前端OAuth回调处理** -4. **实现Token刷新逻辑** -5. **测试登出功能** - -详细文档请参考:`REFRESH_TOKEN_API.md` \ No newline at end of file diff --git a/REFRESH_TOKEN_SUMMARY.md b/REFRESH_TOKEN_SUMMARY.md deleted file mode 100644 index 36ef6f4..0000000 --- a/REFRESH_TOKEN_SUMMARY.md +++ /dev/null @@ -1,174 +0,0 @@ -# 账户登录密钥系统重构完成报告 - -## 📋 项目概述 - -已成功重构ClassworksKV的账户登录密钥系统,从单一JWT令牌升级为标准的Refresh Token系统,大幅提升了安全性和用户体验。 - -## ✅ 完成的工作 - -### 1. 数据库架构更新 -- 在`Account`模型中添加了`refreshToken`、`refreshTokenExpiry`和`tokenVersion`字段 -- 支持令牌版本控制,可快速失效所有设备的令牌 -- 向后兼容现有数据 - -### 2. 核心Token管理系统 -- **创建 `utils/tokenManager.js`**: 全新的令牌管理核心 - - 生成Access Token(15分钟有效期) - - 生成Refresh Token(7天有效期) - - 支持HS256和RS256算法 - - 令牌刷新和撤销功能 - - 安全验证机制 - -- **重构 `utils/jwt.js`**: 保持向后兼容性 - - 重新导出新的令牌管理功能 - - 保留旧版API供现有代码使用 - -### 3. 认证中间件升级 -- **更新 `middleware/jwt-auth.js`**: - - 支持新的Access Token验证 - - 自动检测即将过期的令牌并在响应头提供新令牌 - - 向后兼容旧版JWT令牌 - - 新增可选认证中间件 - -### 4. API端点扩展 -- **更新 `routes/accounts.js`**: - - OAuth回调现在返回令牌对(access_token + refresh_token) - - 新增 `/api/accounts/refresh` - 刷新访问令牌 - - 新增 `/api/accounts/logout` - 单设备登出 - - 新增 `/api/accounts/logout-all` - 全设备登出 - - 新增 `/api/accounts/token-info` - 查看令牌状态 - -### 5. 安全特性 -- **短期Access Token**: 默认15分钟,降低泄露风险 -- **长期Refresh Token**: 默认7天,用户体验友好 -- **令牌版本控制**: 支持立即失效所有设备的令牌 -- **自动刷新机制**: 在令牌即将过期时自动提供新令牌 -- **设备级管理**: 支持单设备或全设备登出 - -## 📚 文档输出 - -### 1. 详细API文档 -**文件**: `REFRESH_TOKEN_API.md` -- 完整的API接口说明 -- 前端集成示例(JavaScript/React) -- 安全考虑和最佳实践 -- 错误处理指南 -- 性能优化建议 - -### 2. 快速使用指南 -**文件**: `REFRESH_TOKEN_QUICKSTART.md` -- 环境配置说明 -- 核心API使用方法 -- 前端集成代码示例 -- 迁移步骤指导 - -## 🔧 配置说明 - -### 环境变量 -```bash -# Access Token配置 -ACCESS_TOKEN_EXPIRES_IN=15m # 访问令牌过期时间 -REFRESH_TOKEN_EXPIRES_IN=7d # 刷新令牌过期时间 - -# 密钥配置 -JWT_SECRET=your-access-token-secret # Access Token密钥 -REFRESH_TOKEN_SECRET=your-refresh-token-secret # Refresh Token密钥 - -# 可选:RSA算法配置 -JWT_ALG=RS256 -ACCESS_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----..." -ACCESS_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----..." -REFRESH_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----..." -REFRESH_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----..." -``` - -## 🚀 部署步骤 - -### 1. 数据库迁移 -```bash -npx prisma migrate dev --name add_refresh_token_system -``` - -### 2. 环境变量更新 -```bash -# 添加新的环境变量到 .env 文件 -echo "ACCESS_TOKEN_EXPIRES_IN=15m" >> .env -echo "REFRESH_TOKEN_EXPIRES_IN=7d" >> .env -echo "REFRESH_TOKEN_SECRET=your-refresh-token-secret-change-this" >> .env -``` - -### 3. 前端更新 -- 更新OAuth回调处理逻辑 -- 实现Token刷新机制 -- 添加自动重试逻辑 - -## 🔄 向后兼容性 - -- ✅ 现有JWT令牌继续有效 -- ✅ 旧版API端点保持不变 -- ✅ 渐进式迁移支持 -- ✅ 中间件自动检测令牌类型 - -## 📊 系统架构 - -``` -┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ 前端应用 │ │ ClassworksKV │ │ 数据库 │ -│ │ │ 服务端 │ │ │ -├─────────────────┤ ├──────────────────┤ ├─────────────────┤ -│ • Token存储 │◄──►│ • OAuth认证 │◄──►│ • Account表 │ -│ • 自动刷新 │ │ • Token生成 │ │ • refreshToken │ -│ • 请求拦截 │ │ • Token验证 │ │ • tokenVersion │ -│ • 错误处理 │ │ • Token刷新 │ │ • 过期时间 │ -└─────────────────┘ └──────────────────┘ └─────────────────┘ -``` - -## 🛡️ 安全增强 - -### 改进前(旧系统) -- 单一JWT令牌 -- 长期有效(7天) -- 泄露风险高 -- 无法远程登出 - -### 改进后(新系统) -- 双令牌系统 -- Access Token短期(15分钟) -- Refresh Token长期(7天) -- 令牌版本控制 -- 设备级管理 -- 自动刷新机制 - -## 📈 性能考虑 - -- **数据库**: 为refreshToken字段添加索引 -- **内存**: Token缓存机制(可选) -- **网络**: 预刷新机制减少延迟 -- **存储**: 定期清理过期令牌 - -## 🧪 测试建议 - -### 功能测试 -1. OAuth登录流程测试 -2. Token刷新功能测试 -3. 登出功能测试 -4. 过期处理测试 - -### 安全测试 -1. 令牌篡改测试 -2. 过期令牌测试 -3. 并发刷新测试 -4. 版本不匹配测试 - -## 📞 后续支持 - -- 监控令牌刷新频率 -- 分析用户登录模式 -- 优化过期时间配置 -- 收集用户反馈 - ---- - -**重构完成时间**: 2025年11月1日 -**文档版本**: v1.0 -**兼容性**: 向后兼容,支持渐进式迁移 \ No newline at end of file diff --git a/SOCKET_API.md b/SOCKET_API.md deleted file mode 100644 index a907cb2..0000000 --- a/SOCKET_API.md +++ /dev/null @@ -1,565 +0,0 @@ -# Socket.IO 实时频道接口文档(前端) - -## 概述 - -ClassworksKV 提供基于 Socket.IO 的实时键值变更通知服务。前端使用 **KV token**(应用安装 token)加入频道,服务端会自动将 token 映射到对应设备的 uuid 房间。**同一设备的不同 token 会被归入同一频道**,因此多个客户端/应用可以共享实时更新。 - -**重要变更**:不再支持直接使用 uuid 加入频道,所有连接必须使用有效的 KV token。 - -## 安装依赖 - -前端项目安装 Socket.IO 客户端: - -```bash -# npm -npm install socket.io-client - -# pnpm -pnpm add socket.io-client - -# yarn -yarn add socket.io-client -``` - -## 连接服务器 - -### 基础连接 - -```typescript -import { io, Socket } from 'socket.io-client'; - -const SERVER_URL = 'http://localhost:3000'; // 替换为实际服务器地址 - -const socket: Socket = io(SERVER_URL, { - transports: ['websocket'], -}); -``` - -### 连接时自动加入频道(推荐) - -在连接握手时通过 query 参数传入 token,自动加入对应设备频道: - -```typescript -const socket = io(SERVER_URL, { - transports: ['websocket'], - query: { - token: '', // 或使用 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: '' }); -``` - ---- - -#### `leave-token` - 使用 token 离开频道 - -离开指定 token 对应的设备频道。 - -**载荷格式:** -```typescript -{ - token?: string; - apptoken?: string; -} -``` - -**示例:** -```typescript -socket.emit('leave-token', { 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(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) { - const socket = ref(null); - const isConnected = ref(false); - const deviceUuid = ref(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
实时监听中...
; -} -``` - ---- - -## 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=` - -**响应:** -```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 到项目仓库。 diff --git a/middleware/device.js b/middleware/device.js index da3909b..96e720d 100644 --- a/middleware/device.js +++ b/middleware/device.js @@ -7,13 +7,11 @@ * 3. passwordMiddleware - 验证设备密码 */ -import {PrismaClient} from "@prisma/client"; +import {prisma} from "../utils/prisma.js"; import errors from "../utils/errors.js"; import {verifyDevicePassword} from "../utils/crypto.js"; import {analyzeDevice} from "../utils/deviceDetector.js"; -const prisma = new PrismaClient(); - /** * 为新设备创建默认的自动登录配置 * @param {number} deviceId - 设备ID diff --git a/middleware/jwt-auth.js b/middleware/jwt-auth.js index 0f48f2b..b4c2620 100644 --- a/middleware/jwt-auth.js +++ b/middleware/jwt-auth.js @@ -8,11 +8,9 @@ import {generateAccessToken, validateAccountToken, verifyAccessToken} from "../utils/tokenManager.js"; import {verifyToken} from "../utils/jwt.js"; -import {PrismaClient} from "@prisma/client"; +import {prisma} from "../utils/prisma.js"; import errors from "../utils/errors.js"; -const prisma = new PrismaClient(); - /** * 新的JWT认证中间件(支持refresh token系统) */ diff --git a/middleware/kvTokenAuth.js b/middleware/kvTokenAuth.js index d3efcae..927022f 100644 --- a/middleware/kvTokenAuth.js +++ b/middleware/kvTokenAuth.js @@ -5,11 +5,9 @@ * 适用于所有KV相关的接口 */ -import {PrismaClient} from "@prisma/client"; +import {prisma} from "../utils/prisma.js"; import errors from "../utils/errors.js"; -const prisma = new PrismaClient(); - /** * KV Token认证中间件 * 从请求中提取token(支持多种方式),验证后将设备和应用信息注入到res.locals diff --git a/middleware/uuidAuth.js b/middleware/uuidAuth.js index e8d95e9..b332dff 100644 --- a/middleware/uuidAuth.js +++ b/middleware/uuidAuth.js @@ -6,13 +6,11 @@ * 3. 适用于需要设备上下文的接口 */ -import {PrismaClient} from "@prisma/client"; +import {prisma} from "../utils/prisma.js"; import errors from "../utils/errors.js"; import {verifyToken as verifyAccountJWT} from "../utils/jwt.js"; import {verifyDevicePassword} from "../utils/crypto.js"; -const prisma = new PrismaClient(); - /** * UUID+密码/JWT混合认证中间件 */ diff --git a/package.json b/package.json index c3fc89b..3bb3f85 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "@opentelemetry/sdk-node": "^0.201.1", "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@prisma/client": "6.16.2", + "@prisma/adapter-pg": "^7.3.0", + "@prisma/client": "^7.3.0", "axios": "^1.9.0", "bcrypt": "^6.0.0", "body-parser": "^2.2.0", @@ -31,11 +32,12 @@ "jsonwebtoken": "^9.0.2", "morgan": "~1.10.0", "node-device-detector": "^2.2.4", + "pg": "^8.18.0", "prom-client": "^15.1.3", "socket.io": "^4.8.1", "uuid": "^11.1.0" }, "devDependencies": { - "prisma": "^6.18.0" + "prisma": "^7.3.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05a8363..23bbea3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,12 @@ importers: '@opentelemetry/semantic-conventions': specifier: ^1.34.0 version: 1.34.0 + '@prisma/adapter-pg': + specifier: ^7.3.0 + version: 7.3.0 '@prisma/client': - specifier: 6.16.2 - version: 6.16.2(prisma@6.18.0(typescript@5.8.3))(typescript@5.8.3) + specifier: ^7.3.0 + version: 7.3.0(prisma@7.3.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.0))(react@19.2.0)(typescript@5.8.3))(typescript@5.8.3) axios: specifier: ^1.9.0 version: 1.9.0(debug@4.4.1) @@ -74,6 +77,9 @@ importers: node-device-detector: specifier: ^2.2.4 version: 2.2.4 + pg: + specifier: ^8.18.0 + version: 8.18.0 prom-client: specifier: ^15.1.3 version: 15.1.3 @@ -85,8 +91,8 @@ importers: version: 11.1.0 devDependencies: prisma: - specifier: ^6.18.0 - version: 6.18.0(typescript@5.8.3) + specifier: ^7.3.0 + version: 7.3.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.0))(react@19.2.0)(typescript@5.8.3) kv-admin: dependencies: @@ -177,6 +183,32 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} + '@chevrotain/cst-dts-gen@10.5.0': + resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==} + + '@chevrotain/gast@10.5.0': + resolution: {integrity: sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==} + + '@chevrotain/types@10.5.0': + resolution: {integrity: sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==} + + '@chevrotain/utils@10.5.0': + resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} + + '@electric-sql/pglite-socket@0.0.20': + resolution: {integrity: sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite-tools@0.2.20': + resolution: {integrity: sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==} + peerDependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite@0.3.15': + resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==} + '@esbuild/aix-ppc64@0.25.10': resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} engines: {node: '>=18'} @@ -354,6 +386,12 @@ packages: engines: {node: '>=6'} hasBin: true + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@internationalized/date@3.9.0': resolution: {integrity: sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg==} @@ -383,6 +421,10 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@mrleebo/prisma-ast@0.13.1': + resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} + engines: {node: '>=16'} + '@opentelemetry/api-logs@0.201.1': resolution: {integrity: sha512-IxcFDP1IGMDemVFG2by/AMK+/o6EuBQ8idUq3xZ6MxgQGeumYZuX5OwR0h9HuvcUc/JPjQGfU5OHKIKYDJcXeA==} engines: {node: '>=8.0.0'} @@ -896,35 +938,63 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 - '@prisma/client@6.16.2': - resolution: {integrity: sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==} - engines: {node: '>=18.18'} + '@prisma/adapter-pg@7.3.0': + resolution: {integrity: sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg==} + + '@prisma/client-runtime-utils@7.3.0': + resolution: {integrity: sha512-dG/ceD9c+tnXATPk8G+USxxYM9E6UdMTnQeQ+1SZUDxTz7SgQcfxEqafqIQHcjdlcNK/pvmmLfSwAs3s2gYwUw==} + + '@prisma/client@7.3.0': + resolution: {integrity: sha512-FXBIxirqQfdC6b6HnNgxGmU7ydCPEPk7maHMOduJJfnTP+MuOGa15X4omjR/zpPUUpm8ef/mEFQjJudOGkXFcQ==} + engines: {node: ^20.19 || ^22.12 || >=24.0} peerDependencies: prisma: '*' - typescript: '>=5.1.0' + typescript: '>=5.4.0' peerDependenciesMeta: prisma: optional: true typescript: optional: true - '@prisma/config@6.18.0': - resolution: {integrity: sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==} + '@prisma/config@7.3.0': + resolution: {integrity: sha512-QyMV67+eXF7uMtKxTEeQqNu/Be7iH+3iDZOQZW5ttfbSwBamCSdwPszA0dum+Wx27I7anYTPLmRmMORKViSW1A==} - '@prisma/debug@6.18.0': - resolution: {integrity: sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==} + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} - '@prisma/engines-version@6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f': - resolution: {integrity: sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==} + '@prisma/debug@7.3.0': + resolution: {integrity: sha512-yh/tHhraCzYkffsI1/3a7SHX8tpgbJu1NPnuxS4rEpJdWAUDHUH25F1EDo6PPzirpyLNkgPPZdhojQK804BGtg==} - '@prisma/engines@6.18.0': - resolution: {integrity: sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==} + '@prisma/dev@0.20.0': + resolution: {integrity: sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==} - '@prisma/fetch-engine@6.18.0': - resolution: {integrity: sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==} + '@prisma/driver-adapter-utils@7.3.0': + resolution: {integrity: sha512-Wdlezh1ck0Rq2dDINkfSkwbR53q53//Eo1vVqVLwtiZ0I6fuWDGNPxwq+SNAIHnsU+FD/m3aIJKevH3vF13U3w==} - '@prisma/get-platform@6.18.0': - resolution: {integrity: sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==} + '@prisma/engines-version@7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735': + resolution: {integrity: sha512-IH2va2ouUHihyiTTRW889LjKAl1CusZOvFfZxCDNpjSENt7g2ndFsK0vdIw/72v7+jCN6YgkHmdAP/BI7SDgyg==} + + '@prisma/engines@7.3.0': + resolution: {integrity: sha512-cWRQoPDXPtR6stOWuWFZf9pHdQ/o8/QNWn0m0zByxf5Kd946Q875XdEJ52pEsX88vOiXUmjuPG3euw82mwQNMg==} + + '@prisma/fetch-engine@7.3.0': + resolution: {integrity: sha512-Mm0F84JMqM9Vxk70pzfNpGJ1lE4hYjOeLMu7nOOD1i83nvp8MSAcFYBnHqLvEZiA6onUR+m8iYogtOY4oPO5lQ==} + + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + + '@prisma/get-platform@7.3.0': + resolution: {integrity: sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg==} + + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + + '@prisma/studio-core@0.13.1': + resolution: {integrity: sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1243,6 +1313,9 @@ packages: '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + '@types/react@19.2.10': + resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==} + '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} @@ -1418,6 +1491,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axios@1.9.0: resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} @@ -1482,6 +1559,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chevrotain@10.5.0: + resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1562,9 +1642,16 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -1602,6 +1689,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1749,6 +1840,10 @@ packages: debug: optional: true + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.2: resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} engines: {node: '>= 6'} @@ -1780,6 +1875,9 @@ packages: resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} engines: {node: '>=14'} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1788,6 +1886,9 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -1807,6 +1908,12 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + grammex@3.1.12: + resolution: {integrity: sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==} + + graphmatch@1.1.0: + resolution: {integrity: sha512-0E62MaTW5rPZVRLyIJZG/YejmdA/Xr1QydHEw3Vt+qOKkMIOE8WDLc9ZX2bmAjtJFZcId4lEdrdmASsEy7D1QA==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1827,6 +1934,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hono@4.11.4: + resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} + engines: {node: '>=16.9.0'} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -1834,6 +1945,9 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -1842,6 +1956,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + import-in-the-middle@1.13.1: resolution: {integrity: sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA==} @@ -1863,6 +1981,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1871,6 +1992,9 @@ packages: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jake@10.9.2: resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} engines: {node: '>=10'} @@ -1965,6 +2089,10 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + local-pkg@1.1.2: resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} engines: {node: '>=14'} @@ -1993,9 +2121,16 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru.min@1.1.3: + resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lucide-react@0.544.0: resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} peerDependencies: @@ -2078,6 +2213,14 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2158,6 +2301,10 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -2171,10 +2318,24 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.11.0: + resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} + pg-pool@3.11.0: + resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.11.0: + resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} + pg-protocol@1.9.5: resolution: {integrity: sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==} @@ -2182,6 +2343,18 @@ packages: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} + pg@8.18.0: + resolution: {integrity: sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2203,6 +2376,10 @@ packages: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} @@ -2215,13 +2392,20 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - prisma@6.18.0: - resolution: {integrity: sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==} - engines: {node: '>=18.18'} + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + prisma@7.3.0: + resolution: {integrity: sha512-ApYSOLHfMN8WftJA+vL6XwAPOh/aZ0BgUyyKPwUFgjARmG6EBI9LzDPf6SWULQMSAxydV9qn5gLj037nPNlg2w==} + engines: {node: ^20.19 || ^22.12 || >=24.0} hasBin: true peerDependencies: - typescript: '>=5.1.0' + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' peerDependenciesMeta: + better-sqlite3: + optional: true typescript: optional: true @@ -2229,6 +2413,9 @@ packages: resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} engines: {node: ^16 || ^18 || >=20} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + protobufjs@7.5.4: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} @@ -2266,6 +2453,11 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + react@19.2.0: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} @@ -2274,11 +2466,17 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + regexp-to-ast@0.5.0: + resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} + reka-ui@2.5.1: resolution: {integrity: sha512-QJGB3q21wQ1Kw28HhhNDpjfFe8qpePX1gK4FTBRd68XTh9aEnhR5bTJnlV0jxi8FBPh0xivZBeNFUc3jiGx7mQ==} peerDependencies: vue: '>= 3.2.0' + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2292,6 +2490,10 @@ packages: engines: {node: '>= 0.4'} hasBin: true + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -2313,6 +2515,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} @@ -2325,6 +2530,9 @@ packages: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -2332,6 +2540,14 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + shimmer@1.2.1: resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} @@ -2351,6 +2567,13 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + socket.io-adapter@2.5.5: resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} @@ -2370,10 +2593,21 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2485,6 +2719,14 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2581,6 +2823,11 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2625,6 +2872,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -2643,6 +2893,31 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@chevrotain/cst-dts-gen@10.5.0': + dependencies: + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + lodash: 4.17.21 + + '@chevrotain/gast@10.5.0': + dependencies: + '@chevrotain/types': 10.5.0 + lodash: 4.17.21 + + '@chevrotain/types@10.5.0': {} + + '@chevrotain/utils@10.5.0': {} + + '@electric-sql/pglite-socket@0.0.20(@electric-sql/pglite@0.3.15)': + dependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite-tools@0.2.20(@electric-sql/pglite@0.3.15)': + dependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite@0.3.15': {} + '@esbuild/aix-ppc64@0.25.10': optional: true @@ -2753,6 +3028,10 @@ snapshots: protobufjs: 7.5.4 yargs: 17.7.2 + '@hono/node-server@1.19.9(hono@4.11.4)': + dependencies: + hono: 4.11.4 + '@internationalized/date@3.9.0': dependencies: '@swc/helpers': 0.5.17 @@ -2786,6 +3065,11 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@mrleebo/prisma-ast@0.13.1': + dependencies: + chevrotain: 10.5.0 + lilconfig: 2.1.0 + '@opentelemetry/api-logs@0.201.1': dependencies: '@opentelemetry/api': 1.9.0 @@ -3533,12 +3817,24 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) - '@prisma/client@6.16.2(prisma@6.18.0(typescript@5.8.3))(typescript@5.8.3)': + '@prisma/adapter-pg@7.3.0': + dependencies: + '@prisma/driver-adapter-utils': 7.3.0 + pg: 8.18.0 + postgres-array: 3.0.4 + transitivePeerDependencies: + - pg-native + + '@prisma/client-runtime-utils@7.3.0': {} + + '@prisma/client@7.3.0(prisma@7.3.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.0))(react@19.2.0)(typescript@5.8.3))(typescript@5.8.3)': + dependencies: + '@prisma/client-runtime-utils': 7.3.0 optionalDependencies: - prisma: 6.18.0(typescript@5.8.3) + prisma: 7.3.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.0))(react@19.2.0)(typescript@5.8.3) typescript: 5.8.3 - '@prisma/config@6.18.0': + '@prisma/config@7.3.0': dependencies: c12: 3.1.0 deepmerge-ts: 7.1.5 @@ -3547,26 +3843,66 @@ snapshots: transitivePeerDependencies: - magicast - '@prisma/debug@6.18.0': {} + '@prisma/debug@7.2.0': {} - '@prisma/engines-version@6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f': {} + '@prisma/debug@7.3.0': {} - '@prisma/engines@6.18.0': + '@prisma/dev@0.20.0(typescript@5.8.3)': dependencies: - '@prisma/debug': 6.18.0 - '@prisma/engines-version': 6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f - '@prisma/fetch-engine': 6.18.0 - '@prisma/get-platform': 6.18.0 + '@electric-sql/pglite': 0.3.15 + '@electric-sql/pglite-socket': 0.0.20(@electric-sql/pglite@0.3.15) + '@electric-sql/pglite-tools': 0.2.20(@electric-sql/pglite@0.3.15) + '@hono/node-server': 1.19.9(hono@4.11.4) + '@mrleebo/prisma-ast': 0.13.1 + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + hono: 4.11.4 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.33.4 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.8.3) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript - '@prisma/fetch-engine@6.18.0': + '@prisma/driver-adapter-utils@7.3.0': dependencies: - '@prisma/debug': 6.18.0 - '@prisma/engines-version': 6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f - '@prisma/get-platform': 6.18.0 + '@prisma/debug': 7.3.0 - '@prisma/get-platform@6.18.0': + '@prisma/engines-version@7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735': {} + + '@prisma/engines@7.3.0': dependencies: - '@prisma/debug': 6.18.0 + '@prisma/debug': 7.3.0 + '@prisma/engines-version': 7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735 + '@prisma/fetch-engine': 7.3.0 + '@prisma/get-platform': 7.3.0 + + '@prisma/fetch-engine@7.3.0': + dependencies: + '@prisma/debug': 7.3.0 + '@prisma/engines-version': 7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735 + '@prisma/get-platform': 7.3.0 + + '@prisma/get-platform@7.2.0': + dependencies: + '@prisma/debug': 7.2.0 + + '@prisma/get-platform@7.3.0': + dependencies: + '@prisma/debug': 7.3.0 + + '@prisma/query-plan-executor@7.2.0': {} + + '@prisma/studio-core@0.13.1(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.0))(react@19.2.0)': + dependencies: + '@types/react': 19.2.10 + react: 19.2.0 + react-dom: 19.2.4(react@19.2.0) '@protobufjs/aspromise@1.1.2': {} @@ -3811,6 +4147,10 @@ snapshots: pg-protocol: 1.9.5 pg-types: 2.2.0 + '@types/react@19.2.10': + dependencies: + csstype: 3.2.3 + '@types/shimmer@1.2.0': {} '@types/tedious@4.0.14': @@ -4032,6 +4372,8 @@ snapshots: asynckit@0.4.0: {} + aws-ssl-profiles@1.1.2: {} + axios@1.9.0(debug@4.4.1): dependencies: follow-redirects: 1.15.9(debug@4.4.1) @@ -4116,6 +4458,15 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chevrotain@10.5.0: + dependencies: + '@chevrotain/cst-dts-gen': 10.5.0 + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + '@chevrotain/utils': 10.5.0 + lodash: 4.17.21 + regexp-to-ast: 0.5.0 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -4184,8 +4535,16 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + csstype@3.1.3: {} + csstype@3.2.3: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -4204,6 +4563,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} destr@2.0.5: {} @@ -4387,6 +4748,11 @@ snapshots: optionalDependencies: debug: 4.4.1 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.2: dependencies: asynckit: 0.4.0 @@ -4425,6 +4791,10 @@ snapshots: - encoding - supports-color + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + get-caller-file@2.0.5: {} get-intrinsic@1.3.0: @@ -4440,6 +4810,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-port-please@3.2.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -4460,6 +4832,10 @@ snapshots: graceful-fs@4.2.11: {} + grammex@3.1.12: {} + + graphmatch@1.1.0: {} + has-flag@4.0.0: {} has-symbols@1.0.3: {} @@ -4474,6 +4850,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hono@4.11.4: {} + hookable@5.5.3: {} http-errors@2.0.0: @@ -4484,6 +4862,8 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-status-codes@2.3.0: {} + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 @@ -4495,6 +4875,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + import-in-the-middle@1.13.1: dependencies: acorn: 8.14.1 @@ -4514,10 +4898,14 @@ snapshots: is-promise@4.0.0: {} + is-property@1.0.2: {} + is-stream@2.0.1: {} is-what@4.1.16: {} + isexe@2.0.0: {} + jake@10.9.2: dependencies: async: 3.2.6 @@ -4604,6 +4992,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 + lilconfig@2.1.0: {} + local-pkg@1.1.2: dependencies: mlly: 1.8.0 @@ -4626,8 +5016,12 @@ snapshots: lodash.once@4.1.1: {} + lodash@4.17.21: {} + long@5.3.2: {} + lru.min@1.1.3: {} + lucide-react@0.544.0(react@19.2.0): dependencies: react: 19.2.0 @@ -4703,6 +5097,22 @@ snapshots: muggle-string@0.4.1: {} + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.3 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.3 + nanoid@3.3.11: {} nanoid@5.1.6: {} @@ -4755,6 +5165,8 @@ snapshots: path-browserify@1.0.1: {} + path-key@3.1.1: {} + path-parse@1.0.7: {} path-to-regexp@8.2.0: {} @@ -4763,8 +5175,19 @@ snapshots: perfect-debounce@1.0.0: {} + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.11.0: {} + pg-int8@1.0.1: {} + pg-pool@3.11.0(pg@8.18.0): + dependencies: + pg: 8.18.0 + + pg-protocol@1.11.0: {} + pg-protocol@1.9.5: {} pg-types@2.2.0: @@ -4775,6 +5198,20 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 + pg@8.18.0: + dependencies: + pg-connection-string: 2.11.0 + pg-pool: 3.11.0(pg@8.18.0) + pg-protocol: 1.11.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@4.0.3: {} @@ -4799,6 +5236,8 @@ snapshots: postgres-array@2.0.0: {} + postgres-array@3.0.4: {} + postgres-bytea@1.0.0: {} postgres-date@1.0.7: {} @@ -4807,20 +5246,35 @@ snapshots: dependencies: xtend: 4.0.2 - prisma@6.18.0(typescript@5.8.3): + postgres@3.4.7: {} + + prisma@7.3.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.0))(react@19.2.0)(typescript@5.8.3): dependencies: - '@prisma/config': 6.18.0 - '@prisma/engines': 6.18.0 + '@prisma/config': 7.3.0 + '@prisma/dev': 0.20.0(typescript@5.8.3) + '@prisma/engines': 7.3.0 + '@prisma/studio-core': 0.13.1(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.0))(react@19.2.0) + mysql2: 3.15.3 + postgres: 3.4.7 optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: + - '@types/react' - magicast + - react + - react-dom prom-client@15.1.3: dependencies: '@opentelemetry/api': 1.9.0 tdigest: 0.1.2 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + protobufjs@7.5.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -4882,10 +5336,17 @@ snapshots: defu: 6.1.4 destr: 2.0.5 + react-dom@19.2.4(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.27.0 + react@19.2.0: {} readdirp@4.1.2: {} + regexp-to-ast@0.5.0: {} + reka-ui@2.5.1(typescript@5.8.3)(vue@3.5.22(typescript@5.8.3)): dependencies: '@floating-ui/dom': 1.7.4 @@ -4903,6 +5364,8 @@ snapshots: - '@vue/composition-api' - typescript + remeda@2.33.4: {} + require-directory@2.1.1: {} require-in-the-middle@7.5.2: @@ -4919,6 +5382,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + retry@0.12.0: {} + rfdc@1.4.1: {} rollup@4.52.3: @@ -4965,6 +5430,8 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.27.0: {} + scule@1.3.0: {} semver@7.7.2: {} @@ -4985,6 +5452,8 @@ snapshots: transitivePeerDependencies: - supports-color + seq-queue@0.0.5: {} + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -4996,6 +5465,12 @@ snapshots: setprototypeof@1.2.0: {} + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + shimmer@1.2.1: {} side-channel-list@1.0.0: @@ -5026,6 +5501,10 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + socket.io-adapter@2.5.5: dependencies: debug: 4.3.7 @@ -5060,8 +5539,14 @@ snapshots: speakingurl@14.0.1: {} + split2@4.2.0: {} + + sqlstring@2.3.3: {} + statuses@2.0.1: {} + std-env@3.10.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5177,6 +5662,10 @@ snapshots: uuid@9.0.1: {} + valibot@1.2.0(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + vary@1.1.2: {} vee-validate@4.15.1(vue@3.5.22(typescript@5.8.3)): @@ -5230,6 +5719,10 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which@2.0.2: + dependencies: + isexe: 2.0.0 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -5260,4 +5753,9 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.12 + graphmatch: 1.1.0 + zod@3.25.76: {} diff --git a/prisma.config.js b/prisma.config.js new file mode 100644 index 0000000..831a20f --- /dev/null +++ b/prisma.config.js @@ -0,0 +1,14 @@ +// This file was generated by Prisma, and assumes you have installed the following: +// npm install --save-dev prisma dotenv +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: process.env["DATABASE_URL"], + }, +}); diff --git a/prisma/migrations/0_init/migration.sql b/prisma/migrations/0_init/migration.sql new file mode 100644 index 0000000..4e105d9 --- /dev/null +++ b/prisma/migrations/0_init/migration.sql @@ -0,0 +1,110 @@ +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "public"; + +-- CreateTable +CREATE TABLE "account" ( + "id" VARCHAR(191) NOT NULL, + "provider" VARCHAR(191) NOT NULL, + "providerid" VARCHAR(191) NOT NULL, + "email" VARCHAR(191), + "name" VARCHAR(191), + "avatarurl" VARCHAR(191), + "providerdata" JSON, + "accesstoken" TEXT, + "createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedat" TIMESTAMPTZ(6) NOT NULL, + "refreshtoken" TEXT, + "refreshtokenexpiry" TIMESTAMPTZ(6), + "tokenversion" INTEGER NOT NULL DEFAULT 1, + + CONSTRAINT "idx_18048_primary" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "appinstall" ( + "id" VARCHAR(191) NOT NULL, + "deviceid" INTEGER NOT NULL, + "appid" VARCHAR(191) NOT NULL, + "token" VARCHAR(191) NOT NULL, + "note" VARCHAR(191), + "installedat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedat" TIMESTAMPTZ(6) NOT NULL, + "devicetype" VARCHAR(191), + "isreadonly" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "idx_18055_primary" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "autoauth" ( + "id" VARCHAR(191) NOT NULL, + "deviceid" INTEGER NOT NULL, + "password" VARCHAR(191), + "devicetype" VARCHAR(191), + "isreadonly" BOOLEAN NOT NULL DEFAULT false, + "createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedat" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "idx_18062_primary" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "device" ( + "id" INTEGER NOT NULL, + "uuid" VARCHAR(191) NOT NULL, + "name" VARCHAR(191), + "accountid" VARCHAR(191), + "createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedat" TIMESTAMPTZ(6) NOT NULL, + "password" VARCHAR(191), + "passwordhint" VARCHAR(191), + "namespace" VARCHAR(191), + + CONSTRAINT "idx_18069_primary" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "kvstore" ( + "deviceid" INTEGER NOT NULL, + "key" VARCHAR(191) NOT NULL, + "value" JSON NOT NULL, + "creatorip" VARCHAR(191) DEFAULT '', + "createdat" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedat" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "idx_18075_primary" PRIMARY KEY ("deviceid","key") +); + +-- CreateIndex +CREATE UNIQUE INDEX "idx_18048_account_provider_providerid_key" ON "account"("provider", "providerid"); + +-- CreateIndex +CREATE UNIQUE INDEX "idx_18055_appinstall_token_key" ON "appinstall"("token"); + +-- CreateIndex +CREATE INDEX "idx_18055_appinstall_deviceid_fkey" ON "appinstall"("deviceid"); + +-- CreateIndex +CREATE UNIQUE INDEX "idx_18062_autoauth_deviceid_password_key" ON "autoauth"("deviceid", "password"); + +-- CreateIndex +CREATE UNIQUE INDEX "idx_18069_device_uuid_key" ON "device"("uuid"); + +-- CreateIndex +CREATE UNIQUE INDEX "idx_18069_device_namespace_key" ON "device"("namespace"); + +-- CreateIndex +CREATE INDEX "idx_18069_device_accountid_fkey" ON "device"("accountid"); + +-- AddForeignKey +ALTER TABLE "appinstall" ADD CONSTRAINT "appinstall_deviceid_fkey" FOREIGN KEY ("deviceid") REFERENCES "device"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "autoauth" ADD CONSTRAINT "autoauth_deviceid_fkey" FOREIGN KEY ("deviceid") REFERENCES "device"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "device" ADD CONSTRAINT "device_accountid_fkey" FOREIGN KEY ("accountid") REFERENCES "account"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "kvstore" ADD CONSTRAINT "kvstore_deviceid_fkey" FOREIGN KEY ("deviceid") REFERENCES "device"("id") ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/prisma/migrations/20251006025039_init/migration.sql b/prisma/migrations/20251006025039_init/migration.sql deleted file mode 100644 index 3c9a235..0000000 --- a/prisma/migrations/20251006025039_init/migration.sql +++ /dev/null @@ -1,68 +0,0 @@ --- CreateTable -CREATE TABLE `KVStore` ( - `deviceId` INTEGER NOT NULL, - `key` VARCHAR(191) NOT NULL, - `value` JSON NOT NULL, - `creatorIp` VARCHAR(191) NULL DEFAULT '', - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - PRIMARY KEY (`deviceId`, `key`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `Account` ( - `id` VARCHAR(191) NOT NULL, - `provider` VARCHAR(191) NOT NULL, - `providerId` VARCHAR(191) NOT NULL, - `email` VARCHAR(191) NULL, - `name` VARCHAR(191) NULL, - `avatarUrl` VARCHAR(191) NULL, - `providerData` JSON NULL, - `accessToken` VARCHAR(191) NOT NULL, - `refreshToken` VARCHAR(191) NULL, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - UNIQUE INDEX `Account_accessToken_key`(`accessToken`), - UNIQUE INDEX `Account_provider_providerId_key`(`provider`, `providerId`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `Device` ( - `id` INTEGER NOT NULL AUTO_INCREMENT, - `uuid` VARCHAR(191) NOT NULL, - `name` VARCHAR(191) NULL, - `accountId` VARCHAR(191) NULL, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - `password` VARCHAR(191) NULL, - `passwordHint` VARCHAR(191) NULL, - - UNIQUE INDEX `Device_uuid_key`(`uuid`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `AppInstall` ( - `id` VARCHAR(191) NOT NULL, - `deviceId` INTEGER NOT NULL, - `appId` VARCHAR(191) NOT NULL, - `token` VARCHAR(191) NOT NULL, - `note` VARCHAR(191) NULL, - `installedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - UNIQUE INDEX `AppInstall_token_key`(`token`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- AddForeignKey -ALTER TABLE `KVStore` ADD CONSTRAINT `KVStore_deviceId_fkey` FOREIGN KEY (`deviceId`) REFERENCES `Device`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE `Device` ADD CONSTRAINT `Device_accountId_fkey` FOREIGN KEY (`accountId`) REFERENCES `Account`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE `AppInstall` ADD CONSTRAINT `AppInstall_deviceId_fkey` FOREIGN KEY (`deviceId`) REFERENCES `Device`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20251007040712_increase_account_refresh_token_length/migration.sql b/prisma/migrations/20251007040712_increase_account_refresh_token_length/migration.sql deleted file mode 100644 index 51eb10a..0000000 --- a/prisma/migrations/20251007040712_increase_account_refresh_token_length/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE `Account` MODIFY `refreshToken` TEXT NULL; diff --git a/prisma/migrations/20251007041015_update/migration.sql b/prisma/migrations/20251007041015_update/migration.sql deleted file mode 100644 index 4635b70..0000000 --- a/prisma/migrations/20251007041015_update/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- DropIndex -DROP INDEX `Account_accessToken_key` ON `Account`; - --- AlterTable -ALTER TABLE `Account` MODIFY `accessToken` TEXT NOT NULL; diff --git a/prisma/migrations/20251007104950_adjust_text_type/migration.sql b/prisma/migrations/20251007104950_adjust_text_type/migration.sql deleted file mode 100644 index af5102c..0000000 --- a/prisma/migrations/20251007104950_adjust_text_type/migration.sql +++ /dev/null @@ -1 +0,0 @@ --- This is an empty migration. \ No newline at end of file diff --git a/prisma/migrations/20251007121656_noneedrefresh_token/migration.sql b/prisma/migrations/20251007121656_noneedrefresh_token/migration.sql deleted file mode 100644 index 2015a0c..0000000 --- a/prisma/migrations/20251007121656_noneedrefresh_token/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `refreshToken` on the `Account` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE `Account` DROP COLUMN `refreshToken`, - MODIFY `accessToken` TEXT NULL; diff --git a/prisma/migrations/20251025113931_add_auto_auth_and_device_updates/migration.sql b/prisma/migrations/20251025113931_add_auto_auth_and_device_updates/migration.sql deleted file mode 100644 index f152f9f..0000000 --- a/prisma/migrations/20251025113931_add_auto_auth_and_device_updates/migration.sql +++ /dev/null @@ -1,47 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[namespace]` on the table `Device` will be added. If there are existing duplicate values, this will fail. - -*/ --- AlterTable -ALTER TABLE `AppInstall` ADD COLUMN `deviceType` VARCHAR(191) NULL, - ADD COLUMN `isReadOnly` BOOLEAN NOT NULL DEFAULT false; - --- AlterTable -ALTER TABLE `Device` ADD COLUMN `namespace` VARCHAR(191) NULL; - --- 将所有设备的 namespace 设置为对应的 uuid 值,避免唯一键冲突 -UPDATE `Device` SET `namespace` = `uuid` WHERE `namespace` IS NULL; - --- CreateTable -CREATE TABLE `AutoAuth` ( - `id` VARCHAR(191) NOT NULL, - `deviceId` INTEGER NOT NULL, - `password` VARCHAR(191) NULL, - `deviceType` VARCHAR(191) NULL, - `isReadOnly` BOOLEAN NOT NULL DEFAULT false, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - UNIQUE INDEX `AutoAuth_deviceId_password_key`(`deviceId`, `password`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- 为每个设备创建默认的 AutoAuth 记录,将 Device.password 复制为 AutoAuth.password -INSERT INTO `AutoAuth` (`id`, `deviceId`, `password`, `deviceType`, `isReadOnly`, `createdAt`, `updatedAt`) -SELECT - CONCAT('autoauth_', UUID()), - `id`, - `password`, - NULL, - false, - CURRENT_TIMESTAMP(3), - CURRENT_TIMESTAMP(3) -FROM `Device`; - --- CreateIndex -CREATE UNIQUE INDEX `Device_namespace_key` ON `Device`(`namespace`); - --- AddForeignKey -ALTER TABLE `AutoAuth` ADD CONSTRAINT `AutoAuth_deviceId_fkey` FOREIGN KEY (`deviceId`) REFERENCES `Device`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20251101131325_add_refresh_token_system/migration.sql b/prisma/migrations/20251101131325_add_refresh_token_system/migration.sql deleted file mode 100644 index eae9af1..0000000 --- a/prisma/migrations/20251101131325_add_refresh_token_system/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ --- AlterTable -ALTER TABLE `Account` ADD COLUMN `refreshToken` TEXT NULL, - ADD COLUMN `refreshTokenExpiry` DATETIME(3) NULL, - ADD COLUMN `tokenVersion` INTEGER NOT NULL DEFAULT 1; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index 592fc0b..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "mysql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7fda1c5..e949251 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,91 +1,85 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-client" + output = "../generated/prisma" } datasource db { - provider = "mysql" - url = env("DATABASE_URL") + provider = "postgresql" } -model KVStore { - deviceId Int - key String - value Json - creatorIp String? @default("") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt +model account { + id String @id(map: "idx_18048_primary") @db.VarChar(191) + provider String @db.VarChar(191) + providerid String @db.VarChar(191) + email String? @db.VarChar(191) + name String? @db.VarChar(191) + avatarurl String? @db.VarChar(191) + providerdata Json? @db.Json + accesstoken String? + createdat DateTime @default(now()) @db.Timestamptz(6) + updatedat DateTime @db.Timestamptz(6) + refreshtoken String? + refreshtokenexpiry DateTime? @db.Timestamptz(6) + tokenversion Int @default(1) + device device[] - // 关联关系 - device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade) - - @@id([deviceId, key]) + @@unique([provider, providerid], map: "idx_18048_account_provider_providerid_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 +model appinstall { + id String @id(map: "idx_18055_primary") @db.VarChar(191) + deviceid Int + appid String @db.VarChar(191) + token String @unique(map: "idx_18055_appinstall_token_key") @db.VarChar(191) + note String? @db.VarChar(191) + installedat DateTime @default(now()) @db.Timestamptz(6) + updatedat DateTime @db.Timestamptz(6) + devicetype String? @db.VarChar(191) + isreadonly Boolean @default(false) + device device @relation(fields: [deviceid], references: [id], onDelete: Cascade) - // 关联的设备 - devices Device[] - - @@unique([provider, providerId]) // 确保同一提供者的用户ID唯一 + @@index([deviceid], map: "idx_18055_appinstall_deviceid_fkey") } -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 // 用户自定义的唯一命名空间 +model autoauth { + id String @id(map: "idx_18062_primary") @db.VarChar(191) + deviceid Int + password String? @db.VarChar(191) + devicetype String? @db.VarChar(191) + isreadonly Boolean @default(false) + createdat DateTime @default(now()) @db.Timestamptz(6) + updatedat DateTime @db.Timestamptz(6) + device device @relation(fields: [deviceid], references: [id], onDelete: Cascade) - // 关联关系 - account Account? @relation(fields: [accountId], references: [id], onDelete: SetNull) - appInstalls AppInstall[] - kvStore KVStore[] // 设备相关的KV存储 - autoAuths AutoAuth[] // 自动授权配置 + @@unique([deviceid, password], map: "idx_18062_autoauth_deviceid_password_key") } -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 +model device { + id Int @id(map: "idx_18069_primary") + uuid String @unique(map: "idx_18069_device_uuid_key") @db.VarChar(191) + name String? @db.VarChar(191) + accountid String? @db.VarChar(191) + createdat DateTime @default(now()) @db.Timestamptz(6) + updatedat DateTime @db.Timestamptz(6) + password String? @db.VarChar(191) + passwordhint String? @db.VarChar(191) + namespace String? @unique(map: "idx_18069_device_namespace_key") @db.VarChar(191) + appinstall appinstall[] + autoauth autoauth[] + account account? @relation(fields: [accountid], references: [id]) + kvstore kvstore[] - // 关联关系 - device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade) + @@index([accountid], map: "idx_18069_device_accountid_fkey") } -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 +model kvstore { + deviceid Int + key String @db.VarChar(191) + value Json @db.Json + creatorip String? @default("") @db.VarChar(191) + createdat DateTime @default(now()) @db.Timestamptz(6) + updatedat DateTime @db.Timestamptz(6) + device device @relation(fields: [deviceid], references: [id], onDelete: Cascade) - // 关联关系 - device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade) - - @@unique([deviceId, password]) // 同一设备的密码必须唯一 + @@id([deviceid, key], map: "idx_18075_primary") } diff --git a/routes/accounts.js b/routes/accounts.js index 29799ef..54895db 100644 --- a/routes/accounts.js +++ b/routes/accounts.js @@ -1,5 +1,5 @@ import {Router} from "express"; -import {PrismaClient} from "@prisma/client"; +import {prisma} from "../utils/prisma.js"; import crypto from "crypto"; import {generateState, getCallbackURL, oauthProviders} from "../config/oauth.js"; import {generateTokenPair, refreshAccessToken, revokeAllTokens, revokeRefreshToken} from "../utils/jwt.js"; @@ -7,7 +7,6 @@ import {jwtAuth} from "../middleware/jwt-auth.js"; import errors from "../utils/errors.js"; const router = Router(); -const prisma = new PrismaClient(); // 存储OAuth state,防止CSRF攻击(生产环境应使用Redis等) const oauthStates = new Map(); diff --git a/routes/apps.js b/routes/apps.js index 52460f1..78c3200 100644 --- a/routes/apps.js +++ b/routes/apps.js @@ -1,14 +1,12 @@ import {Router} from "express"; import {uuidAuth} from "../middleware/uuidAuth.js"; -import {PrismaClient} from "@prisma/client"; +import {prisma} from "../utils/prisma.js"; import crypto from "crypto"; import errors from "../utils/errors.js"; import {verifyDevicePassword} from "../utils/crypto.js"; const router = Router(); -const prisma = new PrismaClient(); - /** * GET /apps/devices/:uuid/apps * 获取设备安装的应用列表 (公开接口,无需认证) @@ -291,7 +289,7 @@ router.post( } // 读取设备的 classworks-list-main 键值 - const kvRecord = await prisma.kVStore.findUnique({ + const kvRecord = await prisma.kvstore.findUnique({ where: { deviceId_key: { deviceId: appInstall.deviceId, diff --git a/routes/auto-auth.js b/routes/auto-auth.js index a16d490..4cadadb 100644 --- a/routes/auto-auth.js +++ b/routes/auto-auth.js @@ -1,12 +1,10 @@ import {Router} from "express"; import {jwtAuth} from "../middleware/jwt-auth.js"; -import {PrismaClient} from "@prisma/client"; +import {prisma} from "../utils/prisma.js"; import errors from "../utils/errors.js"; const router = Router(); -const prisma = new PrismaClient(); - /** * GET /auto-auth/devices/:uuid/auth-configs * 获取设备的所有自动授权配置 (需要 JWT 认证,且设备必须绑定到该账户) diff --git a/routes/device-auth.js b/routes/device-auth.js index 04aa223..6a448d4 100644 --- a/routes/device-auth.js +++ b/routes/device-auth.js @@ -1,10 +1,9 @@ import {Router} from "express"; import deviceCodeStore from "../utils/deviceCodeStore.js"; import errors from "../utils/errors.js"; -import {PrismaClient} from "@prisma/client"; +import {prisma} from "../utils/prisma.js"; const router = Router(); -const prisma = new PrismaClient(); /** diff --git a/routes/device.js b/routes/device.js index db0677b..ab6da5c 100644 --- a/routes/device.js +++ b/routes/device.js @@ -1,14 +1,12 @@ import {Router} from "express"; import {extractDeviceInfo} from "../middleware/uuidAuth.js"; -import {PrismaClient} from "@prisma/client"; +import {prisma} from "../utils/prisma.js"; import errors from "../utils/errors.js"; import {getOnlineDevices} from "../utils/socket.js"; import {registeredDevicesTotal} from "../utils/metrics.js"; const router = Router(); -const prisma = new PrismaClient(); - /** * 为新设备创建默认的自动登录配置 * @param {number} deviceId - 设备ID diff --git a/routes/kv-token.js b/routes/kv-token.js index d67c233..70c1d50 100644 --- a/routes/kv-token.js +++ b/routes/kv-token.js @@ -10,12 +10,10 @@ import { tokenWriteLimiter } from "../middleware/rateLimiter.js"; import errors from "../utils/errors.js"; -import { PrismaClient } from "@prisma/client"; +import { prisma } from "../utils/prisma.js"; const router = Router(); -const prisma = new PrismaClient(); - // 使用KV专用token认证 router.use(kvTokenAuth); diff --git a/utils/kvStore.js b/utils/kvStore.js index a296383..29d5bdb 100644 --- a/utils/kvStore.js +++ b/utils/kvStore.js @@ -1,8 +1,6 @@ -import {PrismaClient} from "@prisma/client"; +import {prisma} from "./prisma.js"; import {keysTotal} from "./metrics.js"; -const prisma = new PrismaClient(); - class KVStore { /** * 通过设备ID和键名获取值 @@ -11,7 +9,7 @@ class KVStore { * @returns {object|null} 键对应的值或null */ async get(deviceId, key) { - const item = await prisma.kVStore.findUnique({ + const item = await prisma.kvstore.findUnique({ where: { deviceId_key: { deviceId: deviceId, @@ -29,7 +27,7 @@ class KVStore { * @returns {object|null} 键的完整信息或null */ async getMetadata(deviceId, key) { - const item = await prisma.kVStore.findUnique({ + const item = await prisma.kvstore.findUnique({ where: { deviceId_key: { deviceId: deviceId, @@ -68,7 +66,7 @@ class KVStore { * @returns {object} 创建或更新的记录 */ async upsert(deviceId, key, value, creatorIp = "") { - const item = await prisma.kVStore.upsert({ + const item = await prisma.kvstore.upsert({ where: { deviceId_key: { deviceId: deviceId, @@ -88,7 +86,7 @@ class KVStore { }); // 更新键总数指标 - const totalKeys = await prisma.kVStore.count(); + const totalKeys = await prisma.kvstore.count(); keysTotal.set(totalKeys); // 返回带有设备ID和原始键的结果 @@ -117,7 +115,7 @@ class KVStore { await prisma.$transaction(async (tx) => { for (const [key, value] of Object.entries(data)) { try { - const item = await tx.kVStore.upsert({ + const item = await tx.kvstore.upsert({ where: { deviceId_key: { deviceId: deviceId, @@ -152,7 +150,7 @@ class KVStore { }); // 在事务完成后,一次性更新指标 - const totalKeys = await prisma.kVStore.count(); + const totalKeys = await prisma.kvstore.count(); keysTotal.set(totalKeys); return { results, errors }; @@ -166,7 +164,7 @@ class KVStore { */ async delete(deviceId, key) { try { - const item = await prisma.kVStore.delete({ + const item = await prisma.kvstore.delete({ where: { deviceId_key: { deviceId: deviceId, @@ -176,7 +174,7 @@ class KVStore { }); // 更新键总数指标 - const totalKeys = await prisma.kVStore.count(); + const totalKeys = await prisma.kvstore.count(); keysTotal.set(totalKeys); return item ? {...item, deviceId, key} : null; @@ -203,7 +201,7 @@ class KVStore { orderBy[sortBy] = sortDir.toLowerCase(); // 查询设备的所有键 - const items = await prisma.kVStore.findMany({ + const items = await prisma.kvstore.findMany({ where: { deviceId: deviceId, }, @@ -246,7 +244,7 @@ class KVStore { orderBy[sortBy] = sortDir.toLowerCase(); // 查询设备的所有键,只选择键名 - const items = await prisma.kVStore.findMany({ + const items = await prisma.kvstore.findMany({ where: { deviceId: deviceId, }, @@ -268,7 +266,7 @@ class KVStore { * @returns {number} 键值对数量 */ async count(deviceId) { - const count = await prisma.kVStore.count({ + const count = await prisma.kvstore.count({ where: { deviceId: deviceId, }, @@ -283,15 +281,15 @@ class KVStore { */ async getStats(deviceId) { const [totalKeys, oldestKey, newestKey] = await Promise.all([ - prisma.kVStore.count({ + prisma.kvstore.count({ where: { deviceId }, }), - prisma.kVStore.findFirst({ + prisma.kvstore.findFirst({ where: { deviceId }, orderBy: { createdAt: "asc" }, select: { createdAt: true, key: true }, }), - prisma.kVStore.findFirst({ + prisma.kvstore.findFirst({ where: { deviceId }, orderBy: { updatedAt: "desc" }, select: { updatedAt: true, key: true }, diff --git a/utils/metrics.js b/utils/metrics.js index 104a137..416564d 100644 --- a/utils/metrics.js +++ b/utils/metrics.js @@ -1,4 +1,5 @@ import client from 'prom-client'; +import { prisma } from './prisma.js'; // 创建自定义注册表(不包含默认指标) const register = new client.Registry(); @@ -27,18 +28,14 @@ export const keysTotal = new client.Gauge({ // 初始化指标数据 export async function initializeMetrics() { try { - const {PrismaClient} = await import('@prisma/client'); - const prisma = new PrismaClient(); - // 获取已注册设备总数 const deviceCount = await prisma.device.count(); registeredDevicesTotal.set(deviceCount); // 获取已创建键总数 - const keyCount = await prisma.kVStore.count(); + const keyCount = await prisma.kvstore.count(); keysTotal.set(keyCount); - await prisma.$disconnect(); console.log('Prometheus metrics initialized - Devices:', deviceCount, 'Keys:', keyCount); } catch (error) { console.error('Failed to initialize metrics:', error); diff --git a/utils/prisma.js b/utils/prisma.js new file mode 100644 index 0000000..89c3bad --- /dev/null +++ b/utils/prisma.js @@ -0,0 +1,10 @@ +import "dotenv/config"; +import { PrismaPg } from '@prisma/adapter-pg' +import { PrismaClient } from '../generated/prisma/client.ts' + +const connectionString = `${process.env.DATABASE_URL}` + +const adapter = new PrismaPg({ connectionString }) +const prisma = new PrismaClient({ adapter }) + +export { prisma } \ No newline at end of file diff --git a/utils/siteinfo.js b/utils/siteinfo.js index c5bd628..d4278d2 100644 --- a/utils/siteinfo.js +++ b/utils/siteinfo.js @@ -1,8 +1,6 @@ -import {PrismaClient} from "@prisma/client"; +import {prisma} from "./prisma.js"; import kvStore from "./kvStore.js"; -const prisma = new PrismaClient(); - // 系统保留UUID用于存储站点信息 const SYSTEM_DEVICE_UUID = "00000000-0000-4000-8000-000000000000"; diff --git a/utils/socket.js b/utils/socket.js index 5e95281..7f812db 100644 --- a/utils/socket.js +++ b/utils/socket.js @@ -13,7 +13,7 @@ */ import { Server } from "socket.io"; -import { PrismaClient } from "@prisma/client"; +import { prisma } from "./prisma.js"; import { onlineDevicesGauge } from "./metrics.js"; import DeviceDetector from "node-device-detector"; import ClientHints from "node-device-detector/client-hints.js"; @@ -38,7 +38,6 @@ const tokenInfoCache = new Map(); // 事件历史记录:每个设备最多保存1000条事件记录 const eventHistory = new Map(); // uuid -> Array const MAX_EVENT_HISTORY = 1000; -const prisma = new PrismaClient(); /** * 检测设备并生成友好的设备名称 diff --git a/utils/tokenManager.js b/utils/tokenManager.js index fc1716f..742648c 100644 --- a/utils/tokenManager.js +++ b/utils/tokenManager.js @@ -1,8 +1,6 @@ import jwt from 'jsonwebtoken'; import crypto from 'crypto'; -import {PrismaClient} from '@prisma/client'; - -const prisma = new PrismaClient(); +import {prisma} from './prisma.js'; // Token 配置 const ACCESS_TOKEN_SECRET = process.env.JWT_SECRET || 'your-access-token-secret-change-this-in-production';