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';