diff --git a/app.js b/app.js
index f616da9..2cac0b0 100644
--- a/app.js
+++ b/app.js
@@ -1,8 +1,8 @@
import "./utils/instrumentation.js";
// import createError from "http-errors";
import express from "express";
-import { join, dirname } from "path";
-import { fileURLToPath } from "url";
+import {dirname, join} from "path";
+import {fileURLToPath} from "url";
// import cookieParser from "cookie-parser";
import logger from "morgan";
import bodyParser from "body-parser";
@@ -15,17 +15,17 @@ import deviceRouter from "./routes/device.js";
import deviceAuthRouter from "./routes/device-auth.js";
import accountsRouter from "./routes/accounts.js";
import autoAuthRouter from "./routes/auto-auth.js";
-import { register } from "./utils/metrics.js";
+import {register} from "./utils/metrics.js";
+import cors from "cors";
var app = express();
-import cors from "cors";
app.options("/{*path}", cors());
app.use(
- cors({
- exposedHeaders: ["ratelimit-policy", "retry-after", "ratelimit"], // 告诉浏览器这些响应头可以暴露
- maxAge: 86400, // 设置OPTIONS请求的结果缓存24小时(86400秒),减少预检请求
- })
+ cors({
+ exposedHeaders: ["ratelimit-policy", "retry-after", "ratelimit"], // 告诉浏览器这些响应头可以暴露
+ maxAge: 86400, // 设置OPTIONS请求的结果缓存24小时(86400秒),减少预检请求
+ })
);
app.disable("x-powered-by");
@@ -36,67 +36,67 @@ const __dirname = dirname(__filename);
// view engine setup
app.set("views", join(__dirname, "views"));
app.set("view engine", "ejs");
-app.use(bodyParser.urlencoded({ extended: true }));
+app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
app.use(logger("dev"));
app.use(express.json());
-app.use(express.urlencoded({ extended: false }));
+app.use(express.urlencoded({extended: false}));
// app.use(cookieParser());
app.use(express.static(join(__dirname, "public")));
// 添加请求超时处理中间件
app.use((req, res, next) => {
- // 设置默认请求超时时间为30秒
- const timeout = 30000;
+ // 设置默认请求超时时间为30秒
+ const timeout = 30000;
- // 设置超时回调
- const timeoutCallback = () => {
- const timeoutError = errors.createError(408, "请求处理超时");
- next(timeoutError);
- };
+ // 设置超时回调
+ const timeoutCallback = () => {
+ const timeoutError = errors.createError(408, "请求处理超时");
+ next(timeoutError);
+ };
- // 设置超时
- req.setTimeout(timeout, timeoutCallback);
+ // 设置超时
+ req.setTimeout(timeout, timeoutCallback);
- // 监听响应完成事件
- res.on("finish", () => {
- // 如果响应已经完成,清除超时处理
- req.setTimeout(0, timeoutCallback);
- });
+ // 监听响应完成事件
+ res.on("finish", () => {
+ // 如果响应已经完成,清除超时处理
+ req.setTimeout(0, timeoutCallback);
+ });
- next();
+ next();
});
app.get("/", (req, res) => {
- res.render("index.ejs");
+ res.render("index.ejs");
});
app.get("/check", (req, res) => {
- res.json({
- status: "success",
- message: "Classworks KV is running",
- time: new Date().getTime(),
- });
+ res.json({
+ status: "success",
+ message: "Classworks KV is running",
+ time: new Date().getTime(),
+ });
});
// Prometheus metrics endpoint with token auth
app.get("/metrics", async (req, res) => {
- try {
- // 检查 token 验证
- const metricsToken = process.env.METRICS_TOKEN;
- if (metricsToken) {
- const providedToken = req.headers.authorization?.replace('Bearer ', '') || req.query.token;
- if (!providedToken || providedToken !== metricsToken) {
- return res.status(401).json({
- error: "Unauthorized",
- message: "Valid metrics token required"
- });
- }
- }
+ try {
+ // 检查 token 验证
+ const metricsToken = process.env.METRICS_TOKEN;
+ if (metricsToken) {
+ const providedToken = req.headers.authorization?.replace('Bearer ', '') || req.query.token;
+ if (!providedToken || providedToken !== metricsToken) {
+ return res.status(401).json({
+ error: "Unauthorized",
+ message: "Valid metrics token required"
+ });
+ }
+ }
- res.set("Content-Type", register.contentType);
- res.end(await register.metrics());
- } catch (err) {
- res.status(500).end(err.message);
- }
+ res.set("Content-Type", register.contentType);
+ res.end(await register.metrics());
+ } catch (err) {
+ res.status(500).end(err.message);
+ }
});
// Mount the Apps router with API rate limiting
@@ -119,8 +119,8 @@ app.use("/accounts", accountsRouter);
// 兜底404路由 - 处理所有未匹配的路由
app.use((req, res, next) => {
- const notFoundError = errors.createError(404, `找不到路径: ${req.path}`);
- next(notFoundError);
+ const notFoundError = errors.createError(404, `找不到路径: ${req.path}`);
+ next(notFoundError);
});
// 全局错误处理中间件
@@ -128,19 +128,19 @@ app.use(errorHandler);
// 全局未捕获的异常处理
process.on("uncaughtException", (error) => {
- console.error("未捕获的异常:", error);
- // 记录错误但不退出进程
+ console.error("未捕获的异常:", error);
+ // 记录错误但不退出进程
});
// 全局未处理的Promise拒绝处理
process.on("unhandledRejection", (reason, promise) => {
- console.error("未处理的Promise拒绝:", reason);
- // 记录错误但不退出进程
+ console.error("未处理的Promise拒绝:", reason);
+ // 记录错误但不退出进程
});
// 处理 SIGTERM 信号
process.on("SIGTERM", () => {
- console.log("收到 SIGTERM 信号,准备关闭服务...");
+ console.log("收到 SIGTERM 信号,准备关闭服务...");
});
export default app;
diff --git a/bin/www b/bin/www
index 8af85ae..964c9d1 100644
--- a/bin/www
+++ b/bin/www
@@ -5,9 +5,9 @@
*/
import app from '../app.js';
-import { createServer } from 'http';
-import { initSocket } from '../utils/socket.js';
-import { initializeMetrics } from '../utils/metrics.js';
+import {createServer} from 'http';
+import {initSocket} from '../utils/socket.js';
+import {initializeMetrics} from '../utils/metrics.js';
/**
* Get port from environment and store in Express.
@@ -41,19 +41,19 @@ server.on('listening', onListening);
*/
function normalizePort(val) {
- var port = parseInt(val, 10);
+ var port = parseInt(val, 10);
- if (isNaN(port)) {
- // named pipe
- return val;
- }
+ if (isNaN(port)) {
+ // named pipe
+ return val;
+ }
- if (port >= 0) {
- // port number
- return port;
- }
+ if (port >= 0) {
+ // port number
+ return port;
+ }
- return false;
+ return false;
}
/**
@@ -61,27 +61,27 @@ function normalizePort(val) {
*/
function onError(error) {
- if (error.syscall !== 'listen') {
- throw error;
- }
+ if (error.syscall !== 'listen') {
+ throw error;
+ }
- var bind = typeof port === 'string'
- ? 'Pipe ' + port
- : 'Port ' + port;
+ var bind = typeof port === 'string'
+ ? 'Pipe ' + port
+ : 'Port ' + port;
- // handle specific listen errors with friendly messages
- switch (error.code) {
- case 'EACCES':
- console.error(bind + ' requires elevated privileges');
- process.exit(1);
- break;
- case 'EADDRINUSE':
- console.error(bind + ' is already in use');
- process.exit(1);
- break;
- default:
- throw error;
- }
+ // handle specific listen errors with friendly messages
+ switch (error.code) {
+ case 'EACCES':
+ console.error(bind + ' requires elevated privileges');
+ process.exit(1);
+ break;
+ case 'EADDRINUSE':
+ console.error(bind + ' is already in use');
+ process.exit(1);
+ break;
+ default:
+ throw error;
+ }
}
/**
@@ -89,6 +89,6 @@ function onError(error) {
*/
function onListening() {
- var addr = server.address();
- console.log(`Server running at http://0.0.0.0:${addr.port}`);
+ var addr = server.address();
+ console.log(`Server running at http://0.0.0.0:${addr.port}`);
}
diff --git a/classworks.js b/classworks.js
index 23f6fb2..b32e76e 100644
--- a/classworks.js
+++ b/classworks.js
@@ -1,5 +1,5 @@
#!/usr/bin/env node
-import { execSync } from "child_process";
+import {execSync} from "child_process";
import dotenv from "dotenv";
dotenv.config();
@@ -7,78 +7,78 @@ dotenv.config();
// 🔄 执行数据库迁移函数
function runDatabaseMigration() {
- try {
- console.log("🔄 执行数据库迁移...");
- execSync("npx prisma migrate deploy", { stdio: "inherit" });
- console.log("✅ 数据库迁移完成");
- } catch (error) {
- console.error("❌ 数据库迁移失败:", error.message);
- process.exit(1);
- }
+ try {
+ console.log("🔄 执行数据库迁移...");
+ execSync("npx prisma migrate deploy", {stdio: "inherit"});
+ console.log("✅ 数据库迁移完成");
+ } catch (error) {
+ console.error("❌ 数据库迁移失败:", error.message);
+ process.exit(1);
+ }
}
// 🧱 数据库初始化函数
function setupDatabase() {
- try {
- // 执行数据库迁移
- runDatabaseMigration();
- } catch (error) {
- console.error("❌ 数据库初始化失败:", error.message);
- process.exit(1);
- }
+ try {
+ // 执行数据库迁移
+ runDatabaseMigration();
+ } catch (error) {
+ console.error("❌ 数据库初始化失败:", error.message);
+ process.exit(1);
+ }
}
// 🔨 本地构建函数
function buildLocal() {
- try {
- // 确保数据库迁移已执行
- runDatabaseMigration();
- execSync("npm install", { stdio: "inherit" }); // 安装依赖
- execSync("npx prisma generate", { stdio: "inherit" }); // 生成 Prisma 客户端
- console.log("✅ 构建完成");
- } catch (error) {
- console.error("❌ 构建失败:", error.message);
- process.exit(1);
- }
+ try {
+ // 确保数据库迁移已执行
+ runDatabaseMigration();
+ execSync("npm install", {stdio: "inherit"}); // 安装依赖
+ execSync("npx prisma generate", {stdio: "inherit"}); // 生成 Prisma 客户端
+ console.log("✅ 构建完成");
+ } catch (error) {
+ console.error("❌ 构建失败:", error.message);
+ process.exit(1);
+ }
}
// 🚀 启动服务函数
function startServer() {
- try {
- execSync("npm run start", { stdio: "inherit" }); // 启动项目
- } catch (error) {
- console.error("❌ 服务启动失败:", error.message);
- process.exit(1);
- }
+ try {
+ execSync("npm run start", {stdio: "inherit"}); // 启动项目
+ } catch (error) {
+ console.error("❌ 服务启动失败:", error.message);
+ process.exit(1);
+ }
}
// ▶️ 执行 Prisma CLI 命令函数
function runPrismaCommand(args) {
- try {
- const command = `npx prisma ${args.join(" ")}`;
- execSync(command, { stdio: "inherit" });
- } catch (error) {
- console.error("❌ Prisma 命令执行失败:", error.message);
- process.exit(1);
- }
+ try {
+ const command = `npx prisma ${args.join(" ")}`;
+ execSync(command, {stdio: "inherit"});
+ } catch (error) {
+ console.error("❌ Prisma 命令执行失败:", error.message);
+ process.exit(1);
+ }
}
// 🧠 主函数,根据命令行参数判断执行哪种流程
async function main() {
- const args = process.argv.slice(2); // 获取命令行参数
- if (args[0] === "prisma") {
- // 如果输入的是 prisma 命令,则执行 prisma 子命令
- runPrismaCommand(args.slice(1));
- } else {
- // 否则按默认流程:初始化 → 构建 → 启动服务
- setupDatabase();
- buildLocal();
- startServer();
- }
+ const args = process.argv.slice(2); // 获取命令行参数
+ if (args[0] === "prisma") {
+ // 如果输入的是 prisma 命令,则执行 prisma 子命令
+ runPrismaCommand(args.slice(1));
+ } else {
+ // 否则按默认流程:初始化 → 构建 → 启动服务
+ setupDatabase();
+ buildLocal();
+ startServer();
+ }
}
// 🚨 捕捉主函数异常
main().catch((error) => {
- console.error("❌ 脚本执行失败:", error);
- process.exit(1);
+ console.error("❌ 脚本执行失败:", error);
+ process.exit(1);
});
diff --git a/cli/get-token-callback.js b/cli/get-token-callback.js
index 4fc9bea..70574d8 100644
--- a/cli/get-token-callback.js
+++ b/cli/get-token-callback.js
@@ -13,127 +13,127 @@
import http from 'http';
import url from 'url';
-import { randomBytes } from 'crypto';
+import {randomBytes} from 'crypto';
// 配置
const CONFIG = {
- // API服务器地址
- baseUrl: process.env.API_BASE_URL || 'http://localhost:3030',
- // 站点密钥
- siteKey: process.env.SITE_KEY || '',
- // 应用ID
- appId: process.env.APP_ID || '1',
- // 授权页面地址(Classworks前端)
- authPageUrl: process.env.FRONTEND_URL,
- // 本地回调服务器端口
- callbackPort: process.env.CALLBACK_PORT || '8080',
- // 回调路径
- callbackPath: '/callback',
- // 超时时间(秒)
- timeout: 300,
+ // API服务器地址
+ baseUrl: process.env.API_BASE_URL || 'http://localhost:3030',
+ // 站点密钥
+ siteKey: process.env.SITE_KEY || '',
+ // 应用ID
+ appId: process.env.APP_ID || '1',
+ // 授权页面地址(Classworks前端)
+ authPageUrl: process.env.FRONTEND_URL,
+ // 本地回调服务器端口
+ callbackPort: process.env.CALLBACK_PORT || '8080',
+ // 回调路径
+ callbackPath: '/callback',
+ // 超时时间(秒)
+ timeout: 300,
};
// 颜色输出
const colors = {
- reset: '\x1b[0m',
- bright: '\x1b[1m',
- dim: '\x1b[2m',
- red: '\x1b[31m',
- green: '\x1b[32m',
- yellow: '\x1b[33m',
- blue: '\x1b[34m',
- cyan: '\x1b[36m',
+ reset: '\x1b[0m',
+ bright: '\x1b[1m',
+ dim: '\x1b[2m',
+ red: '\x1b[31m',
+ green: '\x1b[32m',
+ yellow: '\x1b[33m',
+ blue: '\x1b[34m',
+ cyan: '\x1b[36m',
};
function log(message, color = '') {
- console.log(`${color}${message}${colors.reset}`);
+ console.log(`${color}${message}${colors.reset}`);
}
function logSuccess(message) {
- log(`✓ ${message}`, colors.green);
+ log(`✓ ${message}`, colors.green);
}
function logError(message) {
- log(`✗ ${message}`, colors.red);
+ log(`✗ ${message}`, colors.red);
}
function logInfo(message) {
- log(`ℹ ${message}`, colors.cyan);
+ log(`ℹ ${message}`, colors.cyan);
}
function logWarning(message) {
- log(`⚠ ${message}`, colors.yellow);
+ log(`⚠ ${message}`, colors.yellow);
}
// HTTP请求封装
async function request(path, options = {}) {
- const requestUrl = `${CONFIG.baseUrl}${path}`;
- const headers = {
- 'Content-Type': 'application/json',
- ...options.headers,
- };
+ const requestUrl = `${CONFIG.baseUrl}${path}`;
+ const headers = {
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ };
- if (CONFIG.siteKey) {
- headers['X-Site-Key'] = CONFIG.siteKey;
- }
-
- try {
- const response = await fetch(requestUrl, {
- ...options,
- headers,
- });
-
- const data = await response.json();
-
- if (!response.ok) {
- throw new Error(data.message || `HTTP ${response.status}`);
+ if (CONFIG.siteKey) {
+ headers['X-Site-Key'] = CONFIG.siteKey;
}
- return data;
- } catch (error) {
- if (error.message.includes('fetch')) {
- throw new Error(`无法连接到服务器: ${CONFIG.baseUrl}`);
+ try {
+ const response = await fetch(requestUrl, {
+ ...options,
+ headers,
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || `HTTP ${response.status}`);
+ }
+
+ return data;
+ } catch (error) {
+ if (error.message.includes('fetch')) {
+ throw new Error(`无法连接到服务器: ${CONFIG.baseUrl}`);
+ }
+ throw error;
}
- throw error;
- }
}
// 生成随机状态字符串
function generateState() {
- return randomBytes(16).toString('hex');
+ return randomBytes(16).toString('hex');
}
// 获取设备UUID
async function getDeviceUuid() {
- try {
- const deviceInfo = await request('/device/info');
- return deviceInfo.uuid;
- } catch (error) {
- // 如果设备不存在,生成新的UUID
- const uuid = randomBytes(16).toString('hex');
- logInfo(`生成新的设备UUID: ${uuid}`);
- return uuid;
- }
+ try {
+ const deviceInfo = await request('/device/info');
+ return deviceInfo.uuid;
+ } catch (error) {
+ // 如果设备不存在,生成新的UUID
+ const uuid = randomBytes(16).toString('hex');
+ logInfo(`生成新的设备UUID: ${uuid}`);
+ return uuid;
+ }
}
// 创建回调服务器
function createCallbackServer(state) {
- return new Promise((resolve, reject) => {
- let server;
- let resolved = false;
+ return new Promise((resolve, reject) => {
+ let server;
+ let resolved = false;
- const handleRequest = (req, res) => {
- if (resolved) return;
+ const handleRequest = (req, res) => {
+ if (resolved) return;
- const parsedUrl = url.parse(req.url, true);
+ const parsedUrl = url.parse(req.url, true);
- if (parsedUrl.pathname === CONFIG.callbackPath) {
- const { token, error, state: returnedState } = parsedUrl.query;
+ if (parsedUrl.pathname === CONFIG.callbackPath) {
+ const {token, error, state: returnedState} = parsedUrl.query;
- // 验证状态参数
- if (returnedState !== state) {
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
- res.end(`
+ // 验证状态参数
+ if (returnedState !== state) {
+ res.writeHead(400, {'Content-Type': 'text/html; charset=utf-8'});
+ res.end(`
@@ -151,15 +151,15 @@ function createCallbackServer(state) {
`);
- resolved = true;
- server.close();
- reject(new Error('状态参数不匹配'));
- return;
- }
+ resolved = true;
+ server.close();
+ reject(new Error('状态参数不匹配'));
+ return;
+ }
- if (error) {
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
- res.end(`
+ if (error) {
+ res.writeHead(400, {'Content-Type': 'text/html; charset=utf-8'});
+ res.end(`
@@ -177,15 +177,15 @@ function createCallbackServer(state) {
`);
- resolved = true;
- server.close();
- reject(new Error(error));
- return;
- }
+ resolved = true;
+ server.close();
+ reject(new Error(error));
+ return;
+ }
- if (token) {
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
- res.end(`
+ if (token) {
+ res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
+ res.end(`
@@ -205,15 +205,15 @@ function createCallbackServer(state) {
`);
- resolved = true;
- server.close();
- resolve(token);
- return;
- }
+ resolved = true;
+ server.close();
+ resolve(token);
+ return;
+ }
- // 如果没有token和error参数
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
- res.end(`
+ // 如果没有token和error参数
+ res.writeHead(400, {'Content-Type': 'text/html; charset=utf-8'});
+ res.end(`
@@ -231,191 +231,191 @@ function createCallbackServer(state) {
`);
- resolved = true;
- server.close();
- reject(new Error('缺少必要的参数'));
- } else {
- // 404 for other paths
- res.writeHead(404, { 'Content-Type': 'text/plain' });
- res.end('Not Found');
- }
- };
+ resolved = true;
+ server.close();
+ reject(new Error('缺少必要的参数'));
+ } else {
+ // 404 for other paths
+ res.writeHead(404, {'Content-Type': 'text/plain'});
+ res.end('Not Found');
+ }
+ };
- server = http.createServer(handleRequest);
+ server = http.createServer(handleRequest);
- server.listen(CONFIG.callbackPort, (err) => {
- if (err) {
- reject(err);
- } else {
- logSuccess(`回调服务器已启动: http://localhost:${CONFIG.callbackPort}${CONFIG.callbackPath}`);
- }
+ server.listen(CONFIG.callbackPort, (err) => {
+ if (err) {
+ reject(err);
+ } else {
+ logSuccess(`回调服务器已启动: http://localhost:${CONFIG.callbackPort}${CONFIG.callbackPath}`);
+ }
+ });
+
+ // 设置超时
+ setTimeout(() => {
+ if (!resolved) {
+ resolved = true;
+ server.close();
+ reject(new Error('授权超时'));
+ }
+ }, CONFIG.timeout * 1000);
+
+ server.on('error', (err) => {
+ if (!resolved) {
+ resolved = true;
+ reject(err);
+ }
+ });
});
-
- // 设置超时
- setTimeout(() => {
- if (!resolved) {
- resolved = true;
- server.close();
- reject(new Error('授权超时'));
- }
- }, CONFIG.timeout * 1000);
-
- server.on('error', (err) => {
- if (!resolved) {
- resolved = true;
- reject(err);
- }
- });
- });
}
// 打开浏览器
async function openBrowser(url) {
- const { spawn } = await import('child_process');
+ const {spawn} = await import('child_process');
- let command;
- let args;
+ let command;
+ let args;
- if (process.platform === 'win32') {
- command = 'cmd';
- args = ['/c', 'start', url];
- } else if (process.platform === 'darwin') {
- command = 'open';
- args = [url];
- } else {
- command = 'xdg-open';
- args = [url];
- }
+ if (process.platform === 'win32') {
+ command = 'cmd';
+ args = ['/c', 'start', url];
+ } else if (process.platform === 'darwin') {
+ command = 'open';
+ args = [url];
+ } else {
+ command = 'xdg-open';
+ args = [url];
+ }
- try {
- spawn(command, args, { detached: true, stdio: 'ignore' });
- logSuccess('已尝试打开浏览器');
- } catch (error) {
- logWarning('无法自动打开浏览器,请手动打开授权链接');
- }
+ try {
+ spawn(command, args, {detached: true, stdio: 'ignore'});
+ logSuccess('已尝试打开浏览器');
+ } catch (error) {
+ logWarning('无法自动打开浏览器,请手动打开授权链接');
+ }
}
// 显示授权信息
function displayAuthInfo(authUrl, deviceUuid, state) {
- console.log('\n' + '='.repeat(60));
- log(` 请访问以下地址完成授权:`, colors.bright);
- console.log('');
- log(` ${authUrl}`, colors.cyan + colors.bright);
- console.log('');
- log(` 设备UUID: ${deviceUuid}`, colors.green);
- log(` 状态参数: ${state}`, colors.dim);
- console.log('='.repeat(60));
- logInfo(`回调地址: http://localhost:${CONFIG.callbackPort}${CONFIG.callbackPath}`);
- logInfo(`API服务器: ${CONFIG.baseUrl}`);
- logInfo(`超时时间: ${CONFIG.timeout} 秒`);
- console.log('');
+ console.log('\n' + '='.repeat(60));
+ log(` 请访问以下地址完成授权:`, colors.bright);
+ console.log('');
+ log(` ${authUrl}`, colors.cyan + colors.bright);
+ console.log('');
+ log(` 设备UUID: ${deviceUuid}`, colors.green);
+ log(` 状态参数: ${state}`, colors.dim);
+ console.log('='.repeat(60));
+ logInfo(`回调地址: http://localhost:${CONFIG.callbackPort}${CONFIG.callbackPath}`);
+ logInfo(`API服务器: ${CONFIG.baseUrl}`);
+ logInfo(`超时时间: ${CONFIG.timeout} 秒`);
+ console.log('');
}
// 保存令牌到文件
async function saveToken(token) {
- const fs = await import('fs');
- const path = await import('path');
- const os = await import('os');
+ const fs = await import('fs');
+ const path = await import('path');
+ const os = await import('os');
- const tokenDir = path.join(os.homedir(), '.classworks');
- const tokenFile = path.join(tokenDir, 'token-callback.txt');
+ const tokenDir = path.join(os.homedir(), '.classworks');
+ const tokenFile = path.join(tokenDir, 'token-callback.txt');
- try {
- // 确保目录存在
- if (!fs.existsSync(tokenDir)) {
- fs.mkdirSync(tokenDir, { recursive: true });
+ try {
+ // 确保目录存在
+ if (!fs.existsSync(tokenDir)) {
+ fs.mkdirSync(tokenDir, {recursive: true});
+ }
+
+ // 写入令牌
+ fs.writeFileSync(tokenFile, token, 'utf8');
+ logSuccess(`令牌已保存到: ${tokenFile}`);
+ } catch (error) {
+ logWarning(`无法保存令牌到文件: ${error.message}`);
+ logInfo('您可以手动保存令牌');
}
-
- // 写入令牌
- fs.writeFileSync(tokenFile, token, 'utf8');
- logSuccess(`令牌已保存到: ${tokenFile}`);
- } catch (error) {
- logWarning(`无法保存令牌到文件: ${error.message}`);
- logInfo('您可以手动保存令牌');
- }
}
// 主函数
async function main() {
- console.log('\n' + colors.cyan + colors.bright + '回调授权流程 - 令牌获取工具' + colors.reset + '\n');
+ console.log('\n' + colors.cyan + colors.bright + '回调授权流程 - 令牌获取工具' + colors.reset + '\n');
- try {
- // 检查配置
- if (!CONFIG.siteKey) {
- logWarning('未设置 SITE_KEY 环境变量,可能需要站点密钥才能访问');
- logInfo('设置方法: export SITE_KEY=your-site-key');
- console.log('');
+ try {
+ // 检查配置
+ if (!CONFIG.siteKey) {
+ logWarning('未设置 SITE_KEY 环境变量,可能需要站点密钥才能访问');
+ logInfo('设置方法: export SITE_KEY=your-site-key');
+ console.log('');
+ }
+
+ // 1. 获取设备UUID
+ logInfo('正在获取设备UUID...');
+ const deviceUuid = await getDeviceUuid();
+ logSuccess(`设备UUID: ${deviceUuid}`);
+
+ // 2. 生成状态参数
+ const state = generateState();
+
+ // 3. 构建回调URL
+ const callbackUrl = `http://localhost:${CONFIG.callbackPort}${CONFIG.callbackPath}`;
+
+ // 4. 构建授权URL
+ const authUrl = new URL(CONFIG.authPageUrl);
+ authUrl.searchParams.set('app_id', CONFIG.appId);
+ authUrl.searchParams.set('mode', 'callback');
+ authUrl.searchParams.set('callback_url', callbackUrl);
+ authUrl.searchParams.set('state', state);
+
+ // 5. 显示授权信息
+ displayAuthInfo(authUrl.toString(), deviceUuid, state);
+
+ // 6. 启动回调服务器
+ logInfo('正在启动回调服务器...');
+ const serverPromise = createCallbackServer(state);
+
+ // 7. 打开浏览器
+ logInfo('正在尝试打开浏览器...');
+ await openBrowser(authUrl.toString());
+
+ // 8. 等待授权完成
+ logInfo('等待授权完成...\n');
+ const token = await serverPromise;
+
+ // 9. 显示令牌
+ console.log('\n' + '='.repeat(50));
+ logSuccess('授权成功!令牌获取完成');
+ console.log('='.repeat(50));
+ console.log('\n' + colors.bright + '您的访问令牌:' + colors.reset);
+ log(token, colors.green);
+ console.log('');
+
+ // 10. 保存令牌
+ await saveToken(token);
+
+ // 11. 使用示例
+ console.log('\n' + colors.bright + '使用示例:' + colors.reset);
+ console.log(` curl -H "Authorization: Bearer ${token}" ${CONFIG.baseUrl}/kv`);
+ console.log('');
+
+ process.exit(0);
+ } catch (error) {
+ console.log('');
+ logError(`错误: ${error.message}`);
+
+ // 提供一些常见问题的解决方案
+ if (error.message.includes('EADDRINUSE')) {
+ logInfo(`端口 ${CONFIG.callbackPort} 已被占用,请尝试设置不同的端口:`);
+ logInfo(`CALLBACK_PORT=8081 node cli/get-token-callback.js`);
+ } else if (error.message.includes('无法连接到服务器')) {
+ logInfo('请检查API服务器是否正在运行');
+ logInfo(`当前API地址: ${CONFIG.baseUrl}`);
+ } else if (error.message.includes('授权超时')) {
+ logInfo(`授权超时(${CONFIG.timeout}秒),请重新尝试`);
+ logInfo('您可以设置更长的超时时间:TIMEOUT=600 node cli/get-token-callback.js');
+ }
+
+ console.log('');
+ process.exit(1);
}
-
- // 1. 获取设备UUID
- logInfo('正在获取设备UUID...');
- const deviceUuid = await getDeviceUuid();
- logSuccess(`设备UUID: ${deviceUuid}`);
-
- // 2. 生成状态参数
- const state = generateState();
-
- // 3. 构建回调URL
- const callbackUrl = `http://localhost:${CONFIG.callbackPort}${CONFIG.callbackPath}`;
-
- // 4. 构建授权URL
- const authUrl = new URL(CONFIG.authPageUrl);
- authUrl.searchParams.set('app_id', CONFIG.appId);
- authUrl.searchParams.set('mode', 'callback');
- authUrl.searchParams.set('callback_url', callbackUrl);
- authUrl.searchParams.set('state', state);
-
- // 5. 显示授权信息
- displayAuthInfo(authUrl.toString(), deviceUuid, state);
-
- // 6. 启动回调服务器
- logInfo('正在启动回调服务器...');
- const serverPromise = createCallbackServer(state);
-
- // 7. 打开浏览器
- logInfo('正在尝试打开浏览器...');
- await openBrowser(authUrl.toString());
-
- // 8. 等待授权完成
- logInfo('等待授权完成...\n');
- const token = await serverPromise;
-
- // 9. 显示令牌
- console.log('\n' + '='.repeat(50));
- logSuccess('授权成功!令牌获取完成');
- console.log('='.repeat(50));
- console.log('\n' + colors.bright + '您的访问令牌:' + colors.reset);
- log(token, colors.green);
- console.log('');
-
- // 10. 保存令牌
- await saveToken(token);
-
- // 11. 使用示例
- console.log('\n' + colors.bright + '使用示例:' + colors.reset);
- console.log(` curl -H "Authorization: Bearer ${token}" ${CONFIG.baseUrl}/kv`);
- console.log('');
-
- process.exit(0);
- } catch (error) {
- console.log('');
- logError(`错误: ${error.message}`);
-
- // 提供一些常见问题的解决方案
- if (error.message.includes('EADDRINUSE')) {
- logInfo(`端口 ${CONFIG.callbackPort} 已被占用,请尝试设置不同的端口:`);
- logInfo(`CALLBACK_PORT=8081 node cli/get-token-callback.js`);
- } else if (error.message.includes('无法连接到服务器')) {
- logInfo('请检查API服务器是否正在运行');
- logInfo(`当前API地址: ${CONFIG.baseUrl}`);
- } else if (error.message.includes('授权超时')) {
- logInfo(`授权超时(${CONFIG.timeout}秒),请重新尝试`);
- logInfo('您可以设置更长的超时时间:TIMEOUT=600 node cli/get-token-callback.js');
- }
-
- console.log('');
- process.exit(1);
- }
}
// 运行
diff --git a/cli/get-token.js b/cli/get-token.js
index 786eeed..16ed4f2 100644
--- a/cli/get-token.js
+++ b/cli/get-token.js
@@ -10,223 +10,221 @@
* 或配置为可执行:chmod +x cli/get-token.js && ./cli/get-token.js
*/
-import readline from 'readline';
-
// 配置
const CONFIG = {
- // API服务器地址
- baseUrl: process.env.API_BASE_URL || 'http://localhost:3030',
- // 站点密钥
- siteKey: process.env.SITE_KEY || '',
- // 应用ID
- appId: process.env.APP_ID || '1',
- // 授权页面地址(Classworks前端)
- authPageUrl: process.env.FRONTEND_URL,
- // 轮询间隔(秒)
- pollInterval: 3,
- // 最大轮询次数
- maxPolls: 100,
+ // API服务器地址
+ baseUrl: process.env.API_BASE_URL || 'http://localhost:3030',
+ // 站点密钥
+ siteKey: process.env.SITE_KEY || '',
+ // 应用ID
+ appId: process.env.APP_ID || '1',
+ // 授权页面地址(Classworks前端)
+ authPageUrl: process.env.FRONTEND_URL,
+ // 轮询间隔(秒)
+ pollInterval: 3,
+ // 最大轮询次数
+ maxPolls: 100,
};
// 颜色输出
const colors = {
- reset: '\x1b[0m',
- bright: '\x1b[1m',
- dim: '\x1b[2m',
- red: '\x1b[31m',
- green: '\x1b[32m',
- yellow: '\x1b[33m',
- blue: '\x1b[34m',
- cyan: '\x1b[36m',
+ reset: '\x1b[0m',
+ bright: '\x1b[1m',
+ dim: '\x1b[2m',
+ red: '\x1b[31m',
+ green: '\x1b[32m',
+ yellow: '\x1b[33m',
+ blue: '\x1b[34m',
+ cyan: '\x1b[36m',
};
function log(message, color = '') {
- console.log(`${color}${message}${colors.reset}`);
+ console.log(`${color}${message}${colors.reset}`);
}
function logSuccess(message) {
- log(`✓ ${message}`, colors.green);
+ log(`✓ ${message}`, colors.green);
}
function logError(message) {
- log(`✗ ${message}`, colors.red);
+ log(`✗ ${message}`, colors.red);
}
function logInfo(message) {
- log(`ℹ ${message}`, colors.cyan);
+ log(`ℹ ${message}`, colors.cyan);
}
function logWarning(message) {
- log(`⚠ ${message}`, colors.yellow);
+ log(`⚠ ${message}`, colors.yellow);
}
// HTTP请求封装
async function request(path, options = {}) {
- const url = `${CONFIG.baseUrl}${path}`;
- const headers = {
- 'Content-Type': 'application/json',
- ...options.headers,
- };
+ const url = `${CONFIG.baseUrl}${path}`;
+ const headers = {
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ };
- if (CONFIG.siteKey) {
- headers['X-Site-Key'] = CONFIG.siteKey;
- }
-
- try {
- const response = await fetch(url, {
- ...options,
- headers,
- });
-
- const data = await response.json();
-
- if (!response.ok) {
- throw new Error(data.message || `HTTP ${response.status}`);
+ if (CONFIG.siteKey) {
+ headers['X-Site-Key'] = CONFIG.siteKey;
}
- return data;
- } catch (error) {
- if (error.message.includes('fetch')) {
- throw new Error(`无法连接到服务器: ${CONFIG.baseUrl}`);
+ try {
+ const response = await fetch(url, {
+ ...options,
+ headers,
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || `HTTP ${response.status}`);
+ }
+
+ return data;
+ } catch (error) {
+ if (error.message.includes('fetch')) {
+ throw new Error(`无法连接到服务器: ${CONFIG.baseUrl}`);
+ }
+ throw error;
}
- throw error;
- }
}
// 生成设备代码
async function generateDeviceCode() {
- logInfo('正在生成设备授权码...');
- const data = await request('/auth/device/code', {
- method: 'POST',
- });
- return data;
+ logInfo('正在生成设备授权码...');
+ const data = await request('/auth/device/code', {
+ method: 'POST',
+ });
+ return data;
}
// 轮询获取令牌
async function pollForToken(deviceCode) {
- let polls = 0;
+ let polls = 0;
- return new Promise((resolve, reject) => {
- const poll = async () => {
- polls++;
+ return new Promise((resolve, reject) => {
+ const poll = async () => {
+ polls++;
- if (polls > CONFIG.maxPolls) {
- reject(new Error('轮询超时,请重试'));
- return;
- }
+ if (polls > CONFIG.maxPolls) {
+ reject(new Error('轮询超时,请重试'));
+ return;
+ }
- try {
- const data = await request(`/auth/device/token?device_code=${deviceCode}`);
+ try {
+ const data = await request(`/auth/device/token?device_code=${deviceCode}`);
- if (data.status === 'success') {
- resolve(data.token);
- } else if (data.status === 'expired') {
- reject(new Error('设备代码已过期'));
- } else if (data.status === 'pending') {
- // 继续轮询
- log(`等待授权... (${polls}/${CONFIG.maxPolls})`, colors.dim);
- setTimeout(poll, CONFIG.pollInterval * 1000);
- }
- } catch (error) {
- reject(error);
- }
- };
+ if (data.status === 'success') {
+ resolve(data.token);
+ } else if (data.status === 'expired') {
+ reject(new Error('设备代码已过期'));
+ } else if (data.status === 'pending') {
+ // 继续轮询
+ log(`等待授权... (${polls}/${CONFIG.maxPolls})`, colors.dim);
+ setTimeout(poll, CONFIG.pollInterval * 1000);
+ }
+ } catch (error) {
+ reject(error);
+ }
+ };
- // 开始轮询
- poll();
- });
+ // 开始轮询
+ poll();
+ });
}
// 显示设备代码和授权链接
function displayDeviceCode(deviceCode, expiresIn) {
- console.log('\n' + '='.repeat(60));
- log(` 请访问以下地址完成授权:`, colors.bright);
- console.log('');
+ console.log('\n' + '='.repeat(60));
+ log(` 请访问以下地址完成授权:`, colors.bright);
+ console.log('');
- // 构建授权URL
- const authUrl = `${CONFIG.authPageUrl}?app_id=${CONFIG.appId}&mode=devicecode&devicecode=${deviceCode}`;
- log(` ${authUrl}`, colors.cyan + colors.bright);
- console.log('');
- log(` 设备授权码: ${deviceCode}`, colors.green + colors.bright);
- console.log('='.repeat(60));
- logInfo(`授权码有效期: ${Math.floor(expiresIn / 60)} 分钟`);
- logInfo(`API服务器: ${CONFIG.baseUrl}`);
- console.log('');
+ // 构建授权URL
+ const authUrl = `${CONFIG.authPageUrl}?app_id=${CONFIG.appId}&mode=devicecode&devicecode=${deviceCode}`;
+ log(` ${authUrl}`, colors.cyan + colors.bright);
+ console.log('');
+ log(` 设备授权码: ${deviceCode}`, colors.green + colors.bright);
+ console.log('='.repeat(60));
+ logInfo(`授权码有效期: ${Math.floor(expiresIn / 60)} 分钟`);
+ logInfo(`API服务器: ${CONFIG.baseUrl}`);
+ console.log('');
}
// 保存令牌到文件
async function saveToken(token) {
- const fs = await import('fs');
- const path = await import('path');
- const os = await import('os');
+ const fs = await import('fs');
+ const path = await import('path');
+ const os = await import('os');
- const tokenDir = path.join(os.homedir(), '.classworks');
- const tokenFile = path.join(tokenDir, 'token.txt');
+ const tokenDir = path.join(os.homedir(), '.classworks');
+ const tokenFile = path.join(tokenDir, 'token.txt');
- try {
- // 确保目录存在
- if (!fs.existsSync(tokenDir)) {
- fs.mkdirSync(tokenDir, { recursive: true });
+ try {
+ // 确保目录存在
+ if (!fs.existsSync(tokenDir)) {
+ fs.mkdirSync(tokenDir, {recursive: true});
+ }
+
+ // 写入令牌
+ fs.writeFileSync(tokenFile, token, 'utf8');
+ logSuccess(`令牌已保存到: ${tokenFile}`);
+ } catch (error) {
+ logWarning(`无法保存令牌到文件: ${error.message}`);
+ logInfo('您可以手动保存令牌');
}
-
- // 写入令牌
- fs.writeFileSync(tokenFile, token, 'utf8');
- logSuccess(`令牌已保存到: ${tokenFile}`);
- } catch (error) {
- logWarning(`无法保存令牌到文件: ${error.message}`);
- logInfo('您可以手动保存令牌');
- }
}
// 主函数
async function main() {
- console.log('\n' + colors.cyan + colors.bright + '设备授权流程 - 令牌获取工具' + colors.reset + '\n');
+ console.log('\n' + colors.cyan + colors.bright + '设备授权流程 - 令牌获取工具' + colors.reset + '\n');
- try {
- // 检查配置
- if (!CONFIG.siteKey) {
- logWarning('未设置 SITE_KEY 环境变量,可能需要站点密钥才能访问');
- logInfo('设置方法: export SITE_KEY=your-site-key');
- console.log('');
+ try {
+ // 检查配置
+ if (!CONFIG.siteKey) {
+ logWarning('未设置 SITE_KEY 环境变量,可能需要站点密钥才能访问');
+ logInfo('设置方法: export SITE_KEY=your-site-key');
+ console.log('');
+ }
+
+ // 1. 生成设备代码
+ const {device_code, expires_in} = await generateDeviceCode();
+ logSuccess('设备授权码生成成功!');
+
+ // 2. 显示设备代码和授权链接
+ displayDeviceCode(device_code, expires_in);
+
+ // 3. 提示用户授权
+ logInfo('请在浏览器中打开上述地址,或在授权页面手动输入设备代码');
+ logInfo('等待授权中...\n');
+
+ // 4. 轮询获取令牌
+ const token = await pollForToken(device_code);
+
+ // 5. 显示令牌
+ console.log('\n' + '='.repeat(50));
+ logSuccess('授权成功!令牌获取完成');
+ console.log('='.repeat(50));
+ console.log('\n' + colors.bright + '您的访问令牌:' + colors.reset);
+ log(token, colors.green);
+ console.log('');
+
+ // 6. 保存令牌
+ await saveToken(token);
+
+ // 7. 使用示例
+ console.log('\n' + colors.bright + '使用示例:' + colors.reset);
+ console.log(` curl -H "Authorization: Bearer ${token}" ${CONFIG.baseUrl}/kv`);
+ console.log('');
+
+ process.exit(0);
+ } catch (error) {
+ console.log('');
+ logError(`错误: ${error.message}`);
+ console.log('');
+ process.exit(1);
}
-
- // 1. 生成设备代码
- const { device_code, expires_in } = await generateDeviceCode();
- logSuccess('设备授权码生成成功!');
-
- // 2. 显示设备代码和授权链接
- displayDeviceCode(device_code, expires_in);
-
- // 3. 提示用户授权
- logInfo('请在浏览器中打开上述地址,或在授权页面手动输入设备代码');
- logInfo('等待授权中...\n');
-
- // 4. 轮询获取令牌
- const token = await pollForToken(device_code);
-
- // 5. 显示令牌
- console.log('\n' + '='.repeat(50));
- logSuccess('授权成功!令牌获取完成');
- console.log('='.repeat(50));
- console.log('\n' + colors.bright + '您的访问令牌:' + colors.reset);
- log(token, colors.green);
- console.log('');
-
- // 6. 保存令牌
- await saveToken(token);
-
- // 7. 使用示例
- console.log('\n' + colors.bright + '使用示例:' + colors.reset);
- console.log(` curl -H "Authorization: Bearer ${token}" ${CONFIG.baseUrl}/kv`);
- console.log('');
-
- process.exit(0);
- } catch (error) {
- console.log('');
- logError(`错误: ${error.message}`);
- console.log('');
- process.exit(1);
- }
}
// 运行
diff --git a/config/oauth.js b/config/oauth.js
index 2735a12..53175a4 100644
--- a/config/oauth.js
+++ b/config/oauth.js
@@ -1,82 +1,82 @@
// OAuth 提供者配置
export const oauthProviders = {
- github: {
- clientId: process.env.GITHUB_CLIENT_ID,
- clientSecret: process.env.GITHUB_CLIENT_SECRET,
- authorizationURL: "https://github.com/login/oauth/authorize",
- tokenURL: "https://github.com/login/oauth/access_token",
- userInfoURL: "https://api.github.com/user",
- scope: "read:user user:email",
- // 展示相关
- name: "GitHub",
- displayName: "GitHub",
- icon: "github",
- color: "#24292e",
- description: "使用 GitHub 账号登录",
- website: "https://github.com",
- },
- zerocat: {
- clientId: process.env.ZEROCAT_CLIENT_ID,
- clientSecret: process.env.ZEROCAT_CLIENT_SECRET,
- authorizationURL: "https://zerocat-api.houlangs.com/oauth/authorize",
- tokenURL: "https://zerocat-api.houlangs.com/oauth/token",
- userInfoURL: "https://zerocat-api.houlangs.com/oauth/userinfo",
- scope: "user:basic user:email",
- // 展示相关
- name: "ZeroCat",
- displayName: "ZeroCat",
- icon: "zerocat",
- color: "#415f91",
- description: "使用 ZeroCat 账号登录",
- website: "https://zerocat.dev",
- },
- stcn: {
- // STCN(Casdoor)- 标准 OIDC Provider
- clientId: process.env.STCN_CLIENT_ID,
- clientSecret: process.env.STCN_CLIENT_SECRET,
- // Casdoor 标准端点
- authorizationURL: "https://auth.smart-teach.cn/login/oauth/authorize",
- tokenURL: "https://auth.smart-teach.cn/api/login/oauth/access_token",
- userInfoURL: "https://auth.smart-teach.cn/api/userinfo",
- scope: "openid profile email offline_access",
- // 展示相关
- name: "stcn",
- displayName: "智教联盟账户",
- icon: "casdoor",
- color: "#1068af",
- description: "使用智教联盟账户登录",
- website: "https://auth.smart-teach.cn",
- tokenRequestFormat: "json", // Casdoor 推荐 JSON 提交
- },
- hly: {
- // 厚浪云(Logto) - OIDC Provider
- clientId: process.env.HLY_CLIENT_ID,
- clientSecret: process.env.HLY_CLIENT_SECRET,
- authorizationURL: "https://oauth.houlang.cloud/oidc/auth",
- tokenURL: "https://oauth.houlang.cloud/oidc/token",
- userInfoURL: "https://oauth.houlang.cloud/oidc/me",
- scope: "openid profile email offline_access",
- // 展示相关
- name: "厚浪云",
- displayName: "厚浪云",
- icon: "logto",
- color: "#2d53f8",
- textColor: "#ffffff",
- order: 40,
- description: "使用厚浪云账号登录",
- website: "https://houlang.cloud",
- pkce: true, // 启用PKCE支持
- },
+ github: {
+ clientId: process.env.GITHUB_CLIENT_ID,
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
+ authorizationURL: "https://github.com/login/oauth/authorize",
+ tokenURL: "https://github.com/login/oauth/access_token",
+ userInfoURL: "https://api.github.com/user",
+ scope: "read:user user:email",
+ // 展示相关
+ name: "GitHub",
+ displayName: "GitHub",
+ icon: "github",
+ color: "#24292e",
+ description: "使用 GitHub 账号登录",
+ website: "https://github.com",
+ },
+ zerocat: {
+ clientId: process.env.ZEROCAT_CLIENT_ID,
+ clientSecret: process.env.ZEROCAT_CLIENT_SECRET,
+ authorizationURL: "https://zerocat-api.houlangs.com/oauth/authorize",
+ tokenURL: "https://zerocat-api.houlangs.com/oauth/token",
+ userInfoURL: "https://zerocat-api.houlangs.com/oauth/userinfo",
+ scope: "user:basic user:email",
+ // 展示相关
+ name: "ZeroCat",
+ displayName: "ZeroCat",
+ icon: "zerocat",
+ color: "#415f91",
+ description: "使用 ZeroCat 账号登录",
+ website: "https://zerocat.dev",
+ },
+ stcn: {
+ // STCN(Casdoor)- 标准 OIDC Provider
+ clientId: process.env.STCN_CLIENT_ID,
+ clientSecret: process.env.STCN_CLIENT_SECRET,
+ // Casdoor 标准端点
+ authorizationURL: "https://auth.smart-teach.cn/login/oauth/authorize",
+ tokenURL: "https://auth.smart-teach.cn/api/login/oauth/access_token",
+ userInfoURL: "https://auth.smart-teach.cn/api/userinfo",
+ scope: "openid profile email offline_access",
+ // 展示相关
+ name: "stcn",
+ displayName: "智教联盟账户",
+ icon: "casdoor",
+ color: "#1068af",
+ description: "使用智教联盟账户登录",
+ website: "https://auth.smart-teach.cn",
+ tokenRequestFormat: "json", // Casdoor 推荐 JSON 提交
+ },
+ hly: {
+ // 厚浪云(Logto) - OIDC Provider
+ clientId: process.env.HLY_CLIENT_ID,
+ clientSecret: process.env.HLY_CLIENT_SECRET,
+ authorizationURL: "https://oauth.houlang.cloud/oidc/auth",
+ tokenURL: "https://oauth.houlang.cloud/oidc/token",
+ userInfoURL: "https://oauth.houlang.cloud/oidc/me",
+ scope: "openid profile email offline_access",
+ // 展示相关
+ name: "厚浪云",
+ displayName: "厚浪云",
+ icon: "logto",
+ color: "#2d53f8",
+ textColor: "#ffffff",
+ order: 40,
+ description: "使用厚浪云账号登录",
+ website: "https://houlang.cloud",
+ pkce: true, // 启用PKCE支持
+ },
};
// 获取OAuth回调URL
export function getCallbackURL(provider) {
- const baseUrl = process.env.BASE_URL;
- return `${baseUrl}/accounts/oauth/${provider}/callback`;
+ const baseUrl = process.env.BASE_URL;
+ return `${baseUrl}/accounts/oauth/${provider}/callback`;
}
// 生成随机state参数
export function generateState() {
- return Math.random().toString(36).substring(2, 15) +
- Math.random().toString(36).substring(2, 15);
+ return Math.random().toString(36).substring(2, 15) +
+ Math.random().toString(36).substring(2, 15);
}
\ No newline at end of file
diff --git a/middleware/device.js b/middleware/device.js
index 89545f8..2a8c474 100644
--- a/middleware/device.js
+++ b/middleware/device.js
@@ -7,9 +7,9 @@
* 3. passwordMiddleware - 验证设备密码
*/
-import { PrismaClient } from "@prisma/client";
+import {PrismaClient} from "@prisma/client";
import errors from "../utils/errors.js";
-import { verifyDevicePassword } from "../utils/crypto.js";
+import {verifyDevicePassword} from "../utils/crypto.js";
const prisma = new PrismaClient();
@@ -18,20 +18,20 @@ const prisma = new PrismaClient();
* @param {number} deviceId - 设备ID
*/
async function createDefaultAutoAuth(deviceId) {
- try {
- // 创建默认的自动授权配置:不需要密码、类型是classroom(一体机)
- await prisma.autoAuth.create({
- data: {
- deviceId: deviceId,
- password: null, // 无密码
- deviceType: "classroom", // 一体机类型
- isReadOnly: false, // 非只读
- },
- });
- } catch (error) {
- console.error('创建默认自动登录配置失败:', error);
- // 这里不抛出错误,避免影响设备创建流程
- }
+ try {
+ // 创建默认的自动授权配置:不需要密码、类型是classroom(一体机)
+ await prisma.autoAuth.create({
+ data: {
+ deviceId: deviceId,
+ password: null, // 无密码
+ deviceType: "classroom", // 一体机类型
+ isReadOnly: false, // 非只读
+ },
+ });
+ } catch (error) {
+ console.error('创建默认自动登录配置失败:', error);
+ // 这里不抛出错误,避免影响设备创建流程
+ }
}
/**
@@ -46,36 +46,36 @@ async function createDefaultAutoAuth(deviceId) {
* router.get('/path/:deviceUuid', deviceMiddleware, handler)
*/
export const deviceMiddleware = errors.catchAsync(async (req, res, next) => {
- const deviceUuid = req.params.deviceUuid || req.body.deviceUuid;
+ const deviceUuid = req.params.deviceUuid || req.body.deviceUuid;
- if (!deviceUuid) {
- return next(errors.createError(400, "缺少设备UUID"));
- }
+ if (!deviceUuid) {
+ return next(errors.createError(400, "缺少设备UUID"));
+ }
- // 查找或创建设备
- let device = await prisma.device.findUnique({
- where: { uuid: deviceUuid },
- });
-
- if (!device) {
- // 设备不存在,自动创建
- device = await prisma.device.create({
- data: {
- uuid: deviceUuid,
- name: null,
- password: null,
- passwordHint: null,
- accountId: null,
- },
+ // 查找或创建设备
+ let device = await prisma.device.findUnique({
+ where: {uuid: deviceUuid},
});
- // 为新创建的设备添加默认的自动登录配置
- await createDefaultAutoAuth(device.id);
- }
+ if (!device) {
+ // 设备不存在,自动创建
+ device = await prisma.device.create({
+ data: {
+ uuid: deviceUuid,
+ name: null,
+ password: null,
+ passwordHint: null,
+ accountId: null,
+ },
+ });
- // 将设备信息存储到res.locals
- res.locals.device = device;
- next();
+ // 为新创建的设备添加默认的自动登录配置
+ await createDefaultAutoAuth(device.id);
+ }
+
+ // 将设备信息存储到res.locals
+ res.locals.device = device;
+ next();
});
/**
@@ -89,24 +89,24 @@ export const deviceMiddleware = errors.catchAsync(async (req, res, next) => {
* router.get('/path/:deviceUuid', deviceInfoMiddleware, handler)
*/
export const deviceInfoMiddleware = errors.catchAsync(async (req, res, next) => {
- const deviceUuid = req.params.deviceUuid ;
+ const deviceUuid = req.params.deviceUuid;
- if (!deviceUuid) {
- return next(errors.createError(400, "缺少设备UUID"));
- }
+ if (!deviceUuid) {
+ return next(errors.createError(400, "缺少设备UUID"));
+ }
- // 查找设备
- const device = await prisma.device.findUnique({
- where: { uuid: deviceUuid },
- });
+ // 查找设备
+ const device = await prisma.device.findUnique({
+ where: {uuid: deviceUuid},
+ });
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
+ }
- // 将设备信息存储到res.locals
- res.locals.device = device;
- next();
+ // 将设备信息存储到res.locals
+ res.locals.device = device;
+ next();
});
/**
@@ -122,29 +122,29 @@ export const deviceInfoMiddleware = errors.catchAsync(async (req, res, next) =>
* router.post('/path', deviceMiddleware, passwordMiddleware, handler)
*/
export const passwordMiddleware = errors.catchAsync(async (req, res, next) => {
- const device = res.locals.device;
- const { password } = req.body;
+ const device = res.locals.device;
+ const {password} = req.body;
- if (!device) {
- return next(errors.createError(500, "设备信息未加载,请先使用deviceMiddleware"));
- }
-
- // 如果设备绑定了账户,且请求中有账户信息且匹配,则跳过密码验证
- if (device.accountId && req.account && req.account.id === device.accountId) {
- return next();
- }
-
- // 如果设备有密码,验证密码
- if (device.password) {
- if (!password) {
- return next(errors.createError(401, "设备需要密码"));
+ if (!device) {
+ return next(errors.createError(500, "设备信息未加载,请先使用deviceMiddleware"));
}
- const isValid = await verifyDevicePassword(password, device.password);
- if (!isValid) {
- return next(errors.createError(401, "密码错误"));
+ // 如果设备绑定了账户,且请求中有账户信息且匹配,则跳过密码验证
+ if (device.accountId && req.account && req.account.id === device.accountId) {
+ return next();
}
- }
- next();
+ // 如果设备有密码,验证密码
+ if (device.password) {
+ if (!password) {
+ return next(errors.createError(401, "设备需要密码"));
+ }
+
+ const isValid = await verifyDevicePassword(password, device.password);
+ if (!isValid) {
+ return next(errors.createError(401, "密码错误"));
+ }
+ }
+
+ next();
});
\ No newline at end of file
diff --git a/middleware/errorHandler.js b/middleware/errorHandler.js
index 5129cbd..2b09430 100644
--- a/middleware/errorHandler.js
+++ b/middleware/errorHandler.js
@@ -1,51 +1,51 @@
-import { isDevelopment } from "../utils/config.js";
+import {isDevelopment} from "../utils/config.js";
const errorHandler = (err, req, res, next) => {
- // 判断响应是否已经发送
- if (res.headersSent) {
- return next(err);
- }
- console.error(err);
-
- try {
- if (isDevelopment) {
- // 输出错误信息到控制台
- console.error("Error occurred:");
- console.error(err);
+ // 判断响应是否已经发送
+ if (res.headersSent) {
+ return next(err);
}
- // 提取错误信息
- const statusCode = err.statusCode || err.status || 500;
- const message = err.message || "服务器错误";
- const details = err.details || null;
- const code = err.code || undefined;
+ console.error(err);
- // 返回统一格式的错误响应
- return res.status(statusCode).json({
- success: false,
- message: message,
- code: code,
- details: details,
- error:
- process.env.NODE_ENV === "production"
- ? undefined
- : {
- stack: err.stack,
- originalError: err.originalError
- ? err.originalError.message
- : null,
- },
- });
- } catch (handlerError) {
- // 处理器本身出错的兜底方案
- console.error("Error in error handler:", handlerError);
+ try {
+ if (isDevelopment) {
+ // 输出错误信息到控制台
+ console.error("Error occurred:");
+ console.error(err);
+ }
+ // 提取错误信息
+ const statusCode = err.statusCode || err.status || 500;
+ const message = err.message || "服务器错误";
+ const details = err.details || null;
+ const code = err.code || undefined;
- // 确保能返回响应
- return res.status(500).json({
- success: false,
- message: "服务器错误",
- details: "服务器处理错误时出现问题",
- });
- }
+ // 返回统一格式的错误响应
+ return res.status(statusCode).json({
+ success: false,
+ message: message,
+ code: code,
+ details: details,
+ error:
+ process.env.NODE_ENV === "production"
+ ? undefined
+ : {
+ stack: err.stack,
+ originalError: err.originalError
+ ? err.originalError.message
+ : null,
+ },
+ });
+ } catch (handlerError) {
+ // 处理器本身出错的兜底方案
+ console.error("Error in error handler:", handlerError);
+
+ // 确保能返回响应
+ return res.status(500).json({
+ success: false,
+ message: "服务器错误",
+ details: "服务器处理错误时出现问题",
+ });
+ }
};
export default errorHandler;
diff --git a/middleware/jwt-auth.js b/middleware/jwt-auth.js
index 5d4a6e2..0f48f2b 100644
--- a/middleware/jwt-auth.js
+++ b/middleware/jwt-auth.js
@@ -6,9 +6,9 @@
* 适用于只需要账户验证的接口
*/
-import { verifyAccessToken, validateAccountToken, generateAccessToken } from "../utils/tokenManager.js";
-import { verifyToken } from "../utils/jwt.js";
-import { PrismaClient } from "@prisma/client";
+import {generateAccessToken, validateAccountToken, verifyAccessToken} from "../utils/tokenManager.js";
+import {verifyToken} from "../utils/jwt.js";
+import {PrismaClient} from "@prisma/client";
import errors from "../utils/errors.js";
const prisma = new PrismaClient();
@@ -17,77 +17,77 @@ const prisma = new PrismaClient();
* 新的JWT认证中间件(支持refresh token系统)
*/
export const jwtAuth = async (req, res, next) => {
- try {
- const authHeader = req.headers.authorization;
-
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
- return next(errors.createError(401, "需要提供有效的JWT token"));
- }
-
- const token = authHeader.substring(7);
-
try {
- // 尝试使用新的token验证系统
- const decoded = verifyAccessToken(token);
+ const authHeader = req.headers.authorization;
- // 验证账户并检查token版本
- const account = await validateAccountToken(decoded);
-
- // 将账户信息存储到res.locals
- res.locals.account = account;
- res.locals.tokenDecoded = decoded;
-
- // 检查token是否即将过期(剩余时间少于5分钟)
- const now = Math.floor(Date.now() / 1000);
- const timeUntilExpiry = decoded.exp - now;
-
- if (timeUntilExpiry < 300) { // 5分钟 = 300秒
- // 生成新的access token
- const newAccessToken = generateAccessToken(account);
- res.set('X-New-Access-Token', newAccessToken);
- res.set('X-Token-Refreshed', 'true');
- }
-
- next();
- } catch (newTokenError) {
- // 如果新token系统验证失败,尝试旧的验证方式(向后兼容)
- try {
- const decoded = verifyToken(token);
-
- // 从数据库获取账户信息
- const account = await prisma.account.findUnique({
- where: { id: decoded.accountId },
- });
-
- if (!account) {
- return next(errors.createError(401, "账户不存在"));
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
+ return next(errors.createError(401, "需要提供有效的JWT token"));
}
- // 将账户信息存储到res.locals
- res.locals.account = account;
- res.locals.tokenDecoded = decoded;
- res.locals.isLegacyToken = true; // 标记为旧版token
+ const token = authHeader.substring(7);
- next();
- } catch (legacyTokenError) {
- // 两种验证方式都失败
- if (newTokenError.name === 'JsonWebTokenError' || legacyTokenError.name === 'JsonWebTokenError') {
- return next(errors.createError(401, "无效的JWT token"));
+ try {
+ // 尝试使用新的token验证系统
+ const decoded = verifyAccessToken(token);
+
+ // 验证账户并检查token版本
+ const account = await validateAccountToken(decoded);
+
+ // 将账户信息存储到res.locals
+ res.locals.account = account;
+ res.locals.tokenDecoded = decoded;
+
+ // 检查token是否即将过期(剩余时间少于5分钟)
+ const now = Math.floor(Date.now() / 1000);
+ const timeUntilExpiry = decoded.exp - now;
+
+ if (timeUntilExpiry < 300) { // 5分钟 = 300秒
+ // 生成新的access token
+ const newAccessToken = generateAccessToken(account);
+ res.set('X-New-Access-Token', newAccessToken);
+ res.set('X-Token-Refreshed', 'true');
+ }
+
+ next();
+ } catch (newTokenError) {
+ // 如果新token系统验证失败,尝试旧的验证方式(向后兼容)
+ try {
+ const decoded = verifyToken(token);
+
+ // 从数据库获取账户信息
+ const account = await prisma.account.findUnique({
+ where: {id: decoded.accountId},
+ });
+
+ if (!account) {
+ return next(errors.createError(401, "账户不存在"));
+ }
+
+ // 将账户信息存储到res.locals
+ res.locals.account = account;
+ res.locals.tokenDecoded = decoded;
+ res.locals.isLegacyToken = true; // 标记为旧版token
+
+ next();
+ } catch (legacyTokenError) {
+ // 两种验证方式都失败
+ if (newTokenError.name === 'JsonWebTokenError' || legacyTokenError.name === 'JsonWebTokenError') {
+ return next(errors.createError(401, "无效的JWT token"));
+ }
+
+ if (newTokenError.name === 'TokenExpiredError' || legacyTokenError.name === 'TokenExpiredError') {
+ // 统一的账户JWT过期返回
+ // message: JWT_EXPIRED(用于客户端稳定识别)
+ // code: AUTH_JWT_EXPIRED(业务错误码)
+ return next(errors.createError(401, "JWT_EXPIRED", null, "AUTH_JWT_EXPIRED"));
+ }
+
+ return next(errors.createError(401, "token验证失败"));
+ }
}
-
- if (newTokenError.name === 'TokenExpiredError' || legacyTokenError.name === 'TokenExpiredError') {
- // 统一的账户JWT过期返回
- // message: JWT_EXPIRED(用于客户端稳定识别)
- // code: AUTH_JWT_EXPIRED(业务错误码)
- return next(errors.createError(401, "JWT_EXPIRED", null, "AUTH_JWT_EXPIRED"));
- }
-
- return next(errors.createError(401, "token验证失败"));
- }
+ } catch (error) {
+ return next(errors.createError(500, "认证过程出错"));
}
- } catch (error) {
- return next(errors.createError(500, "认证过程出错"));
- }
};
/**
@@ -95,13 +95,13 @@ export const jwtAuth = async (req, res, next) => {
* 如果提供了token则验证,没有提供则跳过
*/
export const optionalJwtAuth = async (req, res, next) => {
- const authHeader = req.headers.authorization;
+ const authHeader = req.headers.authorization;
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
- // 没有提供token,跳过认证
- return next();
- }
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
+ // 没有提供token,跳过认证
+ return next();
+ }
- // 有token则进行验证
- return jwtAuth(req, res, next);
+ // 有token则进行验证
+ return jwtAuth(req, res, next);
};
\ No newline at end of file
diff --git a/middleware/kvTokenAuth.js b/middleware/kvTokenAuth.js
index e8e06be..d3efcae 100644
--- a/middleware/kvTokenAuth.js
+++ b/middleware/kvTokenAuth.js
@@ -5,7 +5,7 @@
* 适用于所有KV相关的接口
*/
-import { PrismaClient } from "@prisma/client";
+import {PrismaClient} from "@prisma/client";
import errors from "../utils/errors.js";
const prisma = new PrismaClient();
@@ -15,35 +15,35 @@ const prisma = new PrismaClient();
* 从请求中提取token(支持多种方式),验证后将设备和应用信息注入到res.locals
*/
export const kvTokenAuth = async (req, res, next) => {
- try {
- // 从多种途径获取token
- const token = extractToken(req);
+ try {
+ // 从多种途径获取token
+ const token = extractToken(req);
- if (!token) {
- return next(errors.createError(401, "需要提供有效的token"));
+ if (!token) {
+ return next(errors.createError(401, "需要提供有效的token"));
+ }
+
+ // 查找token对应的应用安装信息
+ const appInstall = await prisma.appInstall.findUnique({
+ where: {token},
+ include: {
+ device: true,
+ },
+ });
+
+ if (!appInstall) {
+ return next(errors.createError(401, "无效的token"));
+ }
+
+ // 将信息存储到res.locals供后续使用
+ res.locals.device = appInstall.device;
+ res.locals.appInstall = appInstall;
+ res.locals.deviceId = appInstall.device.id;
+ res.locals.token = token;
+ next();
+ } catch (error) {
+ next(error);
}
-
- // 查找token对应的应用安装信息
- const appInstall = await prisma.appInstall.findUnique({
- where: { token },
- include: {
- device: true,
- },
- });
-
- if (!appInstall) {
- return next(errors.createError(401, "无效的token"));
- }
-
- // 将信息存储到res.locals供后续使用
- res.locals.device = appInstall.device;
- res.locals.appInstall = appInstall;
- res.locals.deviceId = appInstall.device.id;
- res.locals.token = token;
- next();
- } catch (error) {
- next(error);
- }
};
/**
@@ -54,18 +54,18 @@ export const kvTokenAuth = async (req, res, next) => {
* 3. Body: token 或 apptoken
*/
function extractToken(req) {
- // 优先从 Authorization header 提取 Bearer token(支持大小写)
- const authHeader = req.headers && (req.headers.authorization || req.headers.Authorization);
- if (authHeader) {
- const m = authHeader.match(/^Bearer\s+(.+)$/i);
- if (m) return m[1];
- }
+ // 优先从 Authorization header 提取 Bearer token(支持大小写)
+ const authHeader = req.headers && (req.headers.authorization || req.headers.Authorization);
+ if (authHeader) {
+ const m = authHeader.match(/^Bearer\s+(.+)$/i);
+ if (m) return m[1];
+ }
- return (
- req.headers["x-app-token"] ||
- req.query.token ||
- req.query.apptoken ||
- (req.body && req.body.token) ||
- (req.body && req.body.apptoken)
- );
+ return (
+ req.headers["x-app-token"] ||
+ req.query.token ||
+ req.query.apptoken ||
+ (req.body && req.body.token) ||
+ (req.body && req.body.apptoken)
+ );
}
\ No newline at end of file
diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js
index 2d7342f..c7da85b 100644
--- a/middleware/rateLimiter.js
+++ b/middleware/rateLimiter.js
@@ -2,119 +2,118 @@ import rateLimit from "express-rate-limit";
// 获取客户端真实IP的函数
export const getClientIp = (req) => {
- return (
- req.headers["x-forwarded-for"] ||
- req.connection.remoteAddress ||
- req.socket.remoteAddress ||
- req.connection.socket?.remoteAddress ||
- "0.0.0.0"
- );
+ return (
+ req.headers["x-forwarded-for"] ||
+ req.connection.remoteAddress ||
+ req.socket.remoteAddress ||
+ req.connection.socket?.remoteAddress ||
+ "0.0.0.0"
+ );
};
// 从请求中提取Token的函数
const extractToken = (req) => {
- return (
- req.headers["x-app-token"] ||
- req.query.apptoken ||
- req.body?.apptoken ||
- null
- );
+ return (
+ req.headers["x-app-token"] ||
+ req.query.apptoken ||
+ req.body?.apptoken ||
+ null
+ );
};
// 获取限速键:优先使用token,没有token则使用IP
export const getRateLimitKey = (req) => {
- const token = extractToken(req);
- if (token) {
- return `token:${token}`;
- }
- return `ip:${getClientIp(req)}`;
+ const token = extractToken(req);
+ if (token) {
+ return `token:${token}`;
+ }
+ return `ip:${getClientIp(req)}`;
};
// 纯基于Token的keyGenerator,用于KV Token专用路由
// 这个函数假设token已经通过中间件设置在req对象上
export const getTokenOnlyKey = (req) => {
- // 尝试从多个位置获取token
- const token =
- req.locals?.token || // 如果token被设置在req.locals中
- req.res?.locals?.token || // 如果token在res.locals中
- extractToken(req); // 从headers/query/body提取
+ // 尝试从多个位置获取token
+ const token =
+ req.locals?.token || // 如果token被设置在req.locals中
+ req.res?.locals?.token || // 如果token在res.locals中
+ extractToken(req); // 从headers/query/body提取
- if (!token) {
- // 如果没有token,返回一个特殊键用于统一限制
- return "no-token";
- }
- return `token:${token}`;
+ if (!token) {
+ // 如果没有token,返回一个特殊键用于统一限制
+ return "no-token";
+ }
+ return `token:${token}`;
};
// 创建一个中间件来将res.locals.token复制到req.locals.token,以便限速器使用
export const prepareTokenForRateLimit = (req, res, next) => {
- if (res.locals.token) {
- req.locals = req.locals || {};
- req.locals.token = res.locals.token;
- }
- next();
+ if (res.locals.token) {
+ req.locals = req.locals || {};
+ req.locals.token = res.locals.token;
+ }
+ next();
};
-
// 认证相关路由限速器(防止暴力破解)
export const authLimiter = rateLimit({
- windowMs: 30 * 60 * 1000, // 30分钟
- limit: 5, // 每个IP在windowMs时间内最多允许5次认证尝试
- standardHeaders: "draft-7",
- legacyHeaders: false,
- message: "认证请求过于频繁,请30分钟后再试",
- keyGenerator: getClientIp,
- skipSuccessfulRequests: true, // 成功的认证不计入限制
- skipFailedRequests: false, // 失败的认证计入限制
+ windowMs: 30 * 60 * 1000, // 30分钟
+ limit: 5, // 每个IP在windowMs时间内最多允许5次认证尝试
+ standardHeaders: "draft-7",
+ legacyHeaders: false,
+ message: "认证请求过于频繁,请30分钟后再试",
+ keyGenerator: getClientIp,
+ skipSuccessfulRequests: true, // 成功的认证不计入限制
+ skipFailedRequests: false, // 失败的认证计入限制
});
// === Token 专用限速器(更宽松的限制,纯基于Token) ===
// Token 读操作限速器
export const tokenReadLimiter = rateLimit({
- windowMs: 1 * 60 * 1000, // 1分钟
- limit: 1024, // 每个token在1分钟内最多1024次读操作
- standardHeaders: "draft-7",
- legacyHeaders: false,
- message: "读操作请求过于频繁,请稍后再试",
- keyGenerator: getTokenOnlyKey,
- skipSuccessfulRequests: false,
- skipFailedRequests: false,
+ windowMs: 1 * 60 * 1000, // 1分钟
+ limit: 1024, // 每个token在1分钟内最多1024次读操作
+ standardHeaders: "draft-7",
+ legacyHeaders: false,
+ message: "读操作请求过于频繁,请稍后再试",
+ keyGenerator: getTokenOnlyKey,
+ skipSuccessfulRequests: false,
+ skipFailedRequests: false,
});
// Token 写操作限速器
export const tokenWriteLimiter = rateLimit({
- windowMs: 1 * 60 * 1000, // 1分钟
- limit: 512, // 每个token在1分钟内最多512次写操作
- standardHeaders: "draft-7",
- legacyHeaders: false,
- message: "写操作请求过于频繁,请稍后再试",
- keyGenerator: getTokenOnlyKey,
- skipSuccessfulRequests: false,
- skipFailedRequests: false,
+ windowMs: 1 * 60 * 1000, // 1分钟
+ limit: 512, // 每个token在1分钟内最多512次写操作
+ standardHeaders: "draft-7",
+ legacyHeaders: false,
+ message: "写操作请求过于频繁,请稍后再试",
+ keyGenerator: getTokenOnlyKey,
+ skipSuccessfulRequests: false,
+ skipFailedRequests: false,
});
// Token 删除操作限速器
export const tokenDeleteLimiter = rateLimit({
- windowMs: 1 * 60 * 1000, // 1分钟
- limit: 256, // 每个token在1分钟内最多256次删除操作
- standardHeaders: "draft-7",
- legacyHeaders: false,
- message: "删除操作请求过于频繁,请稍后再试",
- keyGenerator: getTokenOnlyKey,
- skipSuccessfulRequests: false,
- skipFailedRequests: false,
+ windowMs: 1 * 60 * 1000, // 1分钟
+ limit: 256, // 每个token在1分钟内最多256次删除操作
+ standardHeaders: "draft-7",
+ legacyHeaders: false,
+ message: "删除操作请求过于频繁,请稍后再试",
+ keyGenerator: getTokenOnlyKey,
+ skipSuccessfulRequests: false,
+ skipFailedRequests: false,
});
// Token 批量操作限速器
export const tokenBatchLimiter = rateLimit({
- windowMs: 1 * 60 * 1000, // 1分钟
- limit: 128, // 每个token在1分钟内最多128次批量操作
- standardHeaders: "draft-7",
- legacyHeaders: false,
- message: "批量操作请求过于频繁,请稍后再试",
- keyGenerator: getTokenOnlyKey,
- skipSuccessfulRequests: false,
- skipFailedRequests: false,
+ windowMs: 1 * 60 * 1000, // 1分钟
+ limit: 128, // 每个token在1分钟内最多128次批量操作
+ standardHeaders: "draft-7",
+ legacyHeaders: false,
+ message: "批量操作请求过于频繁,请稍后再试",
+ keyGenerator: getTokenOnlyKey,
+ skipSuccessfulRequests: false,
+ skipFailedRequests: false,
});
diff --git a/middleware/uuidAuth.js b/middleware/uuidAuth.js
index 7ccaeaf..e8d95e9 100644
--- a/middleware/uuidAuth.js
+++ b/middleware/uuidAuth.js
@@ -6,10 +6,10 @@
* 3. 适用于需要设备上下文的接口
*/
-import { PrismaClient } from "@prisma/client";
+import {PrismaClient} from "@prisma/client";
import errors from "../utils/errors.js";
-import { verifyToken as verifyAccountJWT } from "../utils/jwt.js";
-import { verifyDevicePassword } from "../utils/crypto.js";
+import {verifyToken as verifyAccountJWT} from "../utils/jwt.js";
+import {verifyDevicePassword} from "../utils/crypto.js";
const prisma = new PrismaClient();
@@ -17,130 +17,131 @@ const prisma = new PrismaClient();
* UUID+密码/JWT混合认证中间件
*/
export const uuidAuth = async (req, res, next) => {
- try {
- // 1. 获取UUID(必需)
- const uuid = extractUuid(req);
- if (!uuid) {
- return next(errors.createError(400, "需要提供设备UUID"));
- }
+ try {
+ // 1. 获取UUID(必需)
+ const uuid = extractUuid(req);
+ if (!uuid) {
+ return next(errors.createError(400, "需要提供设备UUID"));
+ }
- // 2. 查找设备并存储到locals
- const device = await prisma.device.findUnique({
- where: { uuid },
- });
-
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
-
- // 存储设备信息到locals
- res.locals.device = device;
- res.locals.deviceId = device.id;
-
- // 3. 验证密码或JWT(二选一)
- const password = extractPassword(req);
- const jwt = extractJWT(req);
-
- if (jwt) {
- // 验证账户JWT
- try {
- const accountPayload = await verifyAccountJWT(jwt);
- const account = await prisma.account.findUnique({
- where: { id: accountPayload.accountId },
- include: {
- devices: {
- where: { uuid },
- select: { id: true }
- }
- }
+ // 2. 查找设备并存储到locals
+ const device = await prisma.device.findUnique({
+ where: {uuid},
});
- if (!account) {
- return next(errors.createError(401, "账户不存在"));
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
}
- // 检查设备是否绑定到此账户
- if (account.devices.length === 0) {
- return next(errors.createError(403, "设备未绑定到此账户"));
+ // 存储设备信息到locals
+ res.locals.device = device;
+ res.locals.deviceId = device.id;
+
+ // 3. 验证密码或JWT(二选一)
+ const password = extractPassword(req);
+ const jwt = extractJWT(req);
+
+ if (jwt) {
+ // 验证账户JWT
+ try {
+ const accountPayload = await verifyAccountJWT(jwt);
+ const account = await prisma.account.findUnique({
+ where: {id: accountPayload.accountId},
+ include: {
+ devices: {
+ where: {uuid},
+ select: {id: true}
+ }
+ }
+ });
+
+ if (!account) {
+ return next(errors.createError(401, "账户不存在"));
+ }
+
+ // 检查设备是否绑定到此账户
+ if (account.devices.length === 0) {
+ return next(errors.createError(403, "设备未绑定到此账户"));
+ }
+
+ res.locals.account = account;
+ res.locals.isAccountOwner = true; // 标记为账户拥有者
+ return next();
+ } catch (error) {
+ return next(errors.createError(401, "无效的JWT token"));
+ }
+ } else if (password) {
+ // 验证设备密码
+ if (!device.password) {
+ return next(); // 如果设备未设置密码,允许无密码访问
+ }
+
+ const isValid = await verifyDevicePassword(password, device.password);
+ if (!isValid) {
+ return next(errors.createError(401, "密码错误"));
+ }
+
+ return next();
+ } else {
+ // 如果设备未设置密码,允许无密码访问
+ if (!device.password) {
+ return next();
+ }
+ return next(errors.createError(401, "需要提供密码或JWT token"));
}
-
- res.locals.account = account;
- res.locals.isAccountOwner = true; // 标记为账户拥有者
- return next();
- } catch (error) {
- return next(errors.createError(401, "无效的JWT token"));
- }
- } else if (password) {
- // 验证设备密码
- if (!device.password) {
- return next(); // 如果设备未设置密码,允许无密码访问
- }
-
- const isValid = await verifyDevicePassword(password, device.password);
- if (!isValid) {
- return next(errors.createError(401, "密码错误"));
- }
-
- return next();
- } else {
- // 如果设备未设置密码,允许无密码访问
- if (!device.password) {
- return next();
- }
- return next(errors.createError(401, "需要提供密码或JWT token"));
+ } catch (error) {
+ next(error);
}
- } catch (error) {
- next(error);
- }
};
-export const extractDeviceInfo = async (req,res,next) => {
- var uuid= extractUuid(req);
+export const extractDeviceInfo = async (req, res, next) => {
+ var uuid = extractUuid(req);
- if (!uuid) {
- throw errors.createError(400, "需要提供设备UUID");
- }
- const device = await prisma.device.findUnique({
- where: { uuid },
- });
- if (!device) {
- throw errors.createError(404, "设备不存在");
- }
- res.locals.device = device;
- res.locals.deviceId = device.id;
- next();
+ if (!uuid) {
+ throw errors.createError(400, "需要提供设备UUID");
+ }
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ });
+ if (!device) {
+ throw errors.createError(404, "设备不存在");
+ }
+ res.locals.device = device;
+ res.locals.deviceId = device.id;
+ next();
}
+
/**
* 从请求中提取UUID
*/
function extractUuid(req) {
- return (
- req.headers["x-device-uuid"] ||
- req.query.uuid ||
- req.params.uuid ||
- req.params.deviceUuid ||
- (req.body && req.body.uuid) ||
- (req.body && req.body.deviceUuid)
- );
+ return (
+ req.headers["x-device-uuid"] ||
+ req.query.uuid ||
+ req.params.uuid ||
+ req.params.deviceUuid ||
+ (req.body && req.body.uuid) ||
+ (req.body && req.body.deviceUuid)
+ );
}
/**
* 从请求中提取密码
*/
function extractPassword(req) {
- return (
- req.headers["x-device-password"] ||
- req.query.password ||
- req.query.currentPassword
- );
+ return (
+ req.headers["x-device-password"] ||
+ req.query.password ||
+ req.query.currentPassword
+ );
}
/**
* 从请求中提取JWT
*/
function extractJWT(req) {
- const authHeader = req.headers.authorization;
- if (authHeader && authHeader.startsWith("Bearer ")) {
- return authHeader.substring(7);
- }
- return null;
+ const authHeader = req.headers.authorization;
+ if (authHeader && authHeader.startsWith("Bearer ")) {
+ return authHeader.substring(7);
+ }
+ return null;
}
\ No newline at end of file
diff --git a/public/auth-error.html b/public/auth-error.html
index 484c20a..13438a9 100644
--- a/public/auth-error.html
+++ b/public/auth-error.html
@@ -1,166 +1,172 @@
-
-
- 登录失败
-
+ .help-text {
+ color: #6b7280;
+ font-size: 0.75rem;
+ margin-top: 1.5rem;
+ padding-top: 1.5rem;
+ border-top: 1px solid #e5e7eb;
+ }
+
-
+
登录失败
-
认证过程中出现错误
-
+
认证过程中出现错误
+
-
返回重试
+
返回重试
- 如果问题持续存在,请检查:
- • OAuth应用配置是否正确
- • 回调URL是否已添加到OAuth应用中
- • 环境变量是否配置正确
+ 如果问题持续存在,请检查:
+ • OAuth应用配置是否正确
+ • 回调URL是否已添加到OAuth应用中
+ • 环境变量是否配置正确
-
+
-
+
\ No newline at end of file
diff --git a/public/auth-success.html b/public/auth-success.html
index 43edce7..f81bd9c 100644
--- a/public/auth-success.html
+++ b/public/auth-success.html
@@ -1,160 +1,160 @@
-
-
- 登录成功
-
+ .countdown {
+ color: #4f46e5;
+ font-weight: bold;
+ }
+
-
+
登录成功
OAuth Provider
-
访问令牌
-
加载中...
+
访问令牌
+
加载中...
- 窗口将在 10 秒后自动关闭
+ 窗口将在 10 秒后自动关闭
-
+
-
+
\ No newline at end of file
diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css
index 9453385..b993201 100644
--- a/public/stylesheets/style.css
+++ b/public/stylesheets/style.css
@@ -1,8 +1,8 @@
body {
- padding: 50px;
- font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
+ padding: 50px;
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
- color: #00B7FF;
+ color: #00B7FF;
}
diff --git a/routes/accounts.js b/routes/accounts.js
index 505546a..395a414 100644
--- a/routes/accounts.js
+++ b/routes/accounts.js
@@ -1,9 +1,9 @@
-import { Router } from "express";
-import { PrismaClient } from "@prisma/client";
+import {Router} from "express";
+import {PrismaClient} from "@prisma/client";
import crypto from "crypto";
-import { oauthProviders, getCallbackURL, generateState } from "../config/oauth.js";
-import { generateAccountToken, generateTokenPair, refreshAccessToken, revokeAllTokens, revokeRefreshToken } from "../utils/jwt.js";
-import { jwtAuth } from "../middleware/jwt-auth.js";
+import {generateState, getCallbackURL, oauthProviders} from "../config/oauth.js";
+import {generateTokenPair, refreshAccessToken, revokeAllTokens, revokeRefreshToken} from "../utils/jwt.js";
+import {jwtAuth} from "../middleware/jwt-auth.js";
import errors from "../utils/errors.js";
const router = Router();
@@ -14,27 +14,27 @@ const oauthStates = new Map();
// 生成PKCE code_verifier 和 code_challenge
function generatePkcePair() {
- const codeVerifier = crypto
- .randomBytes(32)
- .toString("base64")
- .replace(/\+/g, "-")
- .replace(/\//g, "_")
- .replace(/=+$/, "");
- const challenge = crypto
- .createHash("sha256")
- .update(codeVerifier)
- .digest("base64")
- .replace(/\+/g, "-")
- .replace(/\//g, "_")
- .replace(/=+$/, "");
- return { codeVerifier, codeChallenge: challenge };
+ const codeVerifier = crypto
+ .randomBytes(32)
+ .toString("base64")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=+$/, "");
+ const challenge = crypto
+ .createHash("sha256")
+ .update(codeVerifier)
+ .digest("base64")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=+$/, "");
+ return {codeVerifier, codeChallenge: challenge};
}
/**
* 生成安全的访问令牌
*/
function generateAccessToken() {
- return crypto.randomBytes(32).toString("hex");
+ return crypto.randomBytes(32).toString("hex");
}
/**
@@ -42,35 +42,35 @@ function generateAccessToken() {
* GET /accounts/oauth/providers
*/
router.get("/oauth/providers", (req, res) => {
- let providers = [];
+ let providers = [];
- for (const [key, config] of Object.entries(oauthProviders)) {
- // 只返回已配置的提供者
- const pkceAllowed = !!config.pkce;
- if (config.clientId && (config.clientSecret || pkceAllowed)) {
- providers.push({
- id: key,
- name: config.name,
- displayName: config.displayName || config.name,
- icon: config.icon,
- color: config.color, // 向后兼容
- brandColor: config.brandColor || config.color,
- textColor: config.textColor || "#ffffff",
- description: config.description,
- order: typeof config.order === 'number' ? config.order : 9999,
- authUrl: `/accounts/oauth/${key}`, // 前端用于发起认证的URL
- website: config.website,
- });
+ for (const [key, config] of Object.entries(oauthProviders)) {
+ // 只返回已配置的提供者
+ const pkceAllowed = !!config.pkce;
+ if (config.clientId && (config.clientSecret || pkceAllowed)) {
+ providers.push({
+ id: key,
+ name: config.name,
+ displayName: config.displayName || config.name,
+ icon: config.icon,
+ color: config.color, // 向后兼容
+ brandColor: config.brandColor || config.color,
+ textColor: config.textColor || "#ffffff",
+ description: config.description,
+ order: typeof config.order === 'number' ? config.order : 9999,
+ authUrl: `/accounts/oauth/${key}`, // 前端用于发起认证的URL
+ website: config.website,
+ });
+ }
}
- }
- // 按 order 排序(从小到大)
- providers = providers.sort((a, b) => a.order - b.order);
+ // 按 order 排序(从小到大)
+ providers = providers.sort((a, b) => a.order - b.order);
- res.json({
- success: true,
- data: providers,
- });
+ res.json({
+ success: true,
+ data: providers,
+ });
});
/**
@@ -81,75 +81,75 @@ router.get("/oauth/providers", (req, res) => {
* - redirect_uri: 前端回调地址(可选)
*/
router.get("/oauth/:provider", (req, res) => {
- const { provider } = req.params;
- const { redirect_uri } = req.query;
+ const {provider} = req.params;
+ const {redirect_uri} = req.query;
- const providerConfig = oauthProviders[provider];
- if (!providerConfig) {
- return res.status(400).json({
- success: false,
- message: `不支持的OAuth提供者: ${provider}`,
- });
- }
-
- const pkceAllowed = !!providerConfig.pkce;
- if (!providerConfig.clientId || (!providerConfig.clientSecret && !pkceAllowed)) {
- return res.status(500).json({
- success: false,
- message: `OAuth提供者 ${provider} 未配置`,
- });
- }
-
- // 生成state参数
- const state = generateState();
-
- // PKCE: 若启用,为此次会话生成code_verifier/challenge
- let codeChallenge, codeVerifier;
- if (pkceAllowed) {
- const pair = generatePkcePair();
- codeVerifier = pair.codeVerifier;
- codeChallenge = pair.codeChallenge;
- }
-
- // 保存state和redirect_uri(5分钟过期)
- oauthStates.set(state, {
- provider,
- redirect_uri,
- timestamp: Date.now(),
- codeVerifier,
- });
-
- // 清理过期的state(超过5分钟)
- for (const [key, value] of oauthStates.entries()) {
- if (Date.now() - value.timestamp > 5 * 60 * 1000) {
- oauthStates.delete(key);
+ const providerConfig = oauthProviders[provider];
+ if (!providerConfig) {
+ return res.status(400).json({
+ success: false,
+ message: `不支持的OAuth提供者: ${provider}`,
+ });
}
- }
- // 构建授权URL
- const params = new URLSearchParams({
- client_id: providerConfig.clientId,
- redirect_uri: getCallbackURL(provider),
- scope: providerConfig.scope,
- state: state,
- response_type: "code",
- });
+ const pkceAllowed = !!providerConfig.pkce;
+ if (!providerConfig.clientId || (!providerConfig.clientSecret && !pkceAllowed)) {
+ return res.status(500).json({
+ success: false,
+ message: `OAuth提供者 ${provider} 未配置`,
+ });
+ }
- // Google需要额外的参数
- if (provider === "google") {
- params.append("access_type", "offline");
- params.append("prompt", "consent");
- }
+ // 生成state参数
+ const state = generateState();
- if (pkceAllowed && codeChallenge) {
- params.append("code_challenge", codeChallenge);
- params.append("code_challenge_method", "S256");
- }
+ // PKCE: 若启用,为此次会话生成code_verifier/challenge
+ let codeChallenge, codeVerifier;
+ if (pkceAllowed) {
+ const pair = generatePkcePair();
+ codeVerifier = pair.codeVerifier;
+ codeChallenge = pair.codeChallenge;
+ }
- const authUrl = `${providerConfig.authorizationURL}?${params.toString()}`;
+ // 保存state和redirect_uri(5分钟过期)
+ oauthStates.set(state, {
+ provider,
+ redirect_uri,
+ timestamp: Date.now(),
+ codeVerifier,
+ });
- // 重定向到OAuth提供者
- res.redirect(authUrl);
+ // 清理过期的state(超过5分钟)
+ for (const [key, value] of oauthStates.entries()) {
+ if (Date.now() - value.timestamp > 5 * 60 * 1000) {
+ oauthStates.delete(key);
+ }
+ }
+
+ // 构建授权URL
+ const params = new URLSearchParams({
+ client_id: providerConfig.clientId,
+ redirect_uri: getCallbackURL(provider),
+ scope: providerConfig.scope,
+ state: state,
+ response_type: "code",
+ });
+
+ // Google需要额外的参数
+ if (provider === "google") {
+ params.append("access_type", "offline");
+ params.append("prompt", "consent");
+ }
+
+ if (pkceAllowed && codeChallenge) {
+ params.append("code_challenge", codeChallenge);
+ params.append("code_challenge_method", "S256");
+ }
+
+ const authUrl = `${providerConfig.authorizationURL}?${params.toString()}`;
+
+ // 重定向到OAuth提供者
+ res.redirect(authUrl);
});
/**
@@ -157,213 +157,213 @@ router.get("/oauth/:provider", (req, res) => {
* GET /accounts/oauth/:provider/callback
*/
router.get("/oauth/:provider/callback", async (req, res) => {
- const { provider } = req.params;
- const { code, state, error } = req.query;
+ const {provider} = req.params;
+ const {code, state, error} = req.query;
- // 如果OAuth提供者返回错误
- if (error) {
- const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
- const errorUrl = new URL(frontendBaseUrl);
- errorUrl.searchParams.append("error", error);
- errorUrl.searchParams.append("provider", provider);
- errorUrl.searchParams.append("success", "false");
- return res.redirect(errorUrl.toString());
- }
-
- // 验证state
- const stateData = oauthStates.get(state);
- if (!stateData || stateData.provider !== provider) {
- const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
- const errorUrl = new URL(frontendBaseUrl);
- errorUrl.searchParams.append("error", "invalid_state");
- errorUrl.searchParams.append("provider", provider);
- errorUrl.searchParams.append("success", "false");
- return res.redirect(errorUrl.toString());
- }
-
- // 删除已使用的state
- oauthStates.delete(state);
-
- const providerConfig = oauthProviders[provider];
-
- try {
- // 1. 使用授权码换取访问令牌
- let tokenResponse;
- if (providerConfig.tokenRequestFormat === 'json') {
- tokenResponse = await fetch(providerConfig.tokenURL, {
- method: "POST",
- headers: {
- "Accept": "application/json",
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- client_id: providerConfig.clientId,
- ...(providerConfig.clientSecret ? { client_secret: providerConfig.clientSecret } : {}),
- code: code,
- grant_type: "authorization_code",
- redirect_uri: getCallbackURL(provider),
- // PKCE: 携带code_verifier
- ...(stateData?.codeVerifier ? { code_verifier: stateData.codeVerifier } : {}),
- }),
- });
- } else {
- tokenResponse = await fetch(providerConfig.tokenURL, {
- method: "POST",
- headers: {
- "Accept": "application/json",
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body: new URLSearchParams({
- client_id: providerConfig.clientId,
- ...(providerConfig.clientSecret ? { client_secret: providerConfig.clientSecret } : {}),
- code: code,
- grant_type: "authorization_code",
- redirect_uri: getCallbackURL(provider),
- // PKCE: 携带code_verifier
- ...(stateData?.codeVerifier ? { code_verifier: stateData.codeVerifier } : {}),
- }),
- });
+ // 如果OAuth提供者返回错误
+ if (error) {
+ const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
+ const errorUrl = new URL(frontendBaseUrl);
+ errorUrl.searchParams.append("error", error);
+ errorUrl.searchParams.append("provider", provider);
+ errorUrl.searchParams.append("success", "false");
+ return res.redirect(errorUrl.toString());
}
- const tokenData = await tokenResponse.json();
-
- if (!tokenData.access_token) {
- throw new Error("获取访问令牌失败");
+ // 验证state
+ const stateData = oauthStates.get(state);
+ if (!stateData || stateData.provider !== provider) {
+ const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
+ const errorUrl = new URL(frontendBaseUrl);
+ errorUrl.searchParams.append("error", "invalid_state");
+ errorUrl.searchParams.append("provider", provider);
+ errorUrl.searchParams.append("success", "false");
+ return res.redirect(errorUrl.toString());
}
- // 2. 使用访问令牌获取用户信息
- let userResponse;
- // Casdoor 支持两种方式:Authorization Bearer 或 accessToken 查询参数
- if (provider === 'stcn') {
- const url = new URL(providerConfig.userInfoURL);
- url.searchParams.set('accessToken', tokenData.access_token);
- userResponse = await fetch(url, { headers: { "Accept": "application/json" } });
- } else {
- userResponse = await fetch(providerConfig.userInfoURL, {
- headers: {
- "Authorization": `Bearer ${tokenData.access_token}`,
- "Accept": "application/json",
- },
- });
+ // 删除已使用的state
+ oauthStates.delete(state);
+
+ const providerConfig = oauthProviders[provider];
+
+ try {
+ // 1. 使用授权码换取访问令牌
+ let tokenResponse;
+ if (providerConfig.tokenRequestFormat === 'json') {
+ tokenResponse = await fetch(providerConfig.tokenURL, {
+ method: "POST",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ client_id: providerConfig.clientId,
+ ...(providerConfig.clientSecret ? {client_secret: providerConfig.clientSecret} : {}),
+ code: code,
+ grant_type: "authorization_code",
+ redirect_uri: getCallbackURL(provider),
+ // PKCE: 携带code_verifier
+ ...(stateData?.codeVerifier ? {code_verifier: stateData.codeVerifier} : {}),
+ }),
+ });
+ } else {
+ tokenResponse = await fetch(providerConfig.tokenURL, {
+ method: "POST",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: new URLSearchParams({
+ client_id: providerConfig.clientId,
+ ...(providerConfig.clientSecret ? {client_secret: providerConfig.clientSecret} : {}),
+ code: code,
+ grant_type: "authorization_code",
+ redirect_uri: getCallbackURL(provider),
+ // PKCE: 携带code_verifier
+ ...(stateData?.codeVerifier ? {code_verifier: stateData.codeVerifier} : {}),
+ }),
+ });
+ }
+
+ const tokenData = await tokenResponse.json();
+
+ if (!tokenData.access_token) {
+ throw new Error("获取访问令牌失败");
+ }
+
+ // 2. 使用访问令牌获取用户信息
+ let userResponse;
+ // Casdoor 支持两种方式:Authorization Bearer 或 accessToken 查询参数
+ if (provider === 'stcn') {
+ const url = new URL(providerConfig.userInfoURL);
+ url.searchParams.set('accessToken', tokenData.access_token);
+ userResponse = await fetch(url, {headers: {"Accept": "application/json"}});
+ } else {
+ userResponse = await fetch(providerConfig.userInfoURL, {
+ headers: {
+ "Authorization": `Bearer ${tokenData.access_token}`,
+ "Accept": "application/json",
+ },
+ });
+ }
+
+ const userData = await userResponse.json();
+
+ // 3. 标准化用户数据(不同提供者返回的字段不同)
+ let normalizedUser = {};
+
+ if (provider === "github") {
+ normalizedUser = {
+ providerId: String(userData.id),
+ email: userData.email,
+ name: userData.name || userData.login,
+ avatarUrl: userData.avatar_url,
+ };
+ } else if (provider === "zerocat") {
+ normalizedUser = {
+ providerId: userData.openid,
+ email: userData.email_verified ? userData.email : null,
+ name: userData.nickname || userData.username,
+ avatarUrl: userData.avatar,
+ };
+ } else if (provider === "hly") {
+ // 厚浪云(Logto)标准OIDC用户信息
+ normalizedUser = {
+ providerId: userData.sub,
+ email: userData.email_verified ? userData.email : null,
+ name: userData.name || userData.preferred_username || userData.nickname,
+ avatarUrl: userData.picture,
+ };
+ } else if (provider === "stcn") {
+ // STCN(Casdoor)标准OIDC用户信息
+ normalizedUser = {
+ providerId: userData.sub,
+ email: userData.email_verified ? userData.email : userData.email || null,
+ name: userData.name || userData.preferred_username || userData.nickname,
+ avatarUrl: userData.picture,
+ };
+ }
+
+ // 名称为空时,用邮箱@前部分回填(若邮箱可用)
+ if ((!normalizedUser.name || normalizedUser.name.trim() === "") && normalizedUser.email) {
+ const at = normalizedUser.email.indexOf("@");
+ if (at > 0) {
+ normalizedUser.name = normalizedUser.email.substring(0, at);
+ }
+ }
+
+ // 4. 查找或创建账户
+ let account = await prisma.account.findUnique({
+ where: {
+ provider_providerId: {
+ provider,
+ providerId: normalizedUser.providerId,
+ },
+ },
+ });
+
+ if (account) {
+ // 更新账户信息
+ account = await prisma.account.update({
+ where: {id: account.id},
+ data: {
+ email: normalizedUser.email || account.email,
+ name: normalizedUser.name || account.name,
+ avatarUrl: normalizedUser.avatarUrl || account.avatarUrl,
+ providerData: userData,
+ //refreshToken: tokenData.refresh_token || account.refreshToken,
+ updatedAt: new Date(),
+ },
+ });
+ } else {
+ // 创建新账户
+ const accessToken = generateAccessToken();
+ account = await prisma.account.create({
+ data: {
+ provider,
+ providerId: normalizedUser.providerId,
+ email: normalizedUser.email,
+ name: normalizedUser.name,
+ avatarUrl: normalizedUser.avatarUrl,
+ providerData: userData,
+ accessToken,
+ //refreshToken: tokenData.refresh_token,
+ },
+ });
+ }
+
+ // 5. 生成令牌对(访问令牌 + 刷新令牌)
+ const tokens = await generateTokenPair(account);
+
+ // 6. 重定向到前端根路径,携带JWT token
+ const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
+ const callbackUrl = new URL(frontendBaseUrl);
+ callbackUrl.searchParams.append("access_token", tokens.accessToken);
+ callbackUrl.searchParams.append("refresh_token", tokens.refreshToken);
+ callbackUrl.searchParams.append("expires_in", tokens.accessTokenExpiresIn);
+ callbackUrl.searchParams.append("provider", provider);
+ // 附带展示信息,便于前端显示品牌与名称
+ const pconf = oauthProviders[provider] || {};
+ callbackUrl.searchParams.append("providerName", pconf.displayName || pconf.name || provider);
+ if (pconf.brandColor || pconf.color) {
+ callbackUrl.searchParams.append("providerColor", pconf.brandColor || pconf.color);
+ }
+ callbackUrl.searchParams.append("success", "true");
+
+ res.redirect(callbackUrl.toString());
+
+ } catch (error) {
+ console.error(`OAuth回调处理失败 [${provider}]:`, error);
+
+ // 重定向到前端根路径,携带错误信息
+ const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
+ const errorUrl = new URL(frontendBaseUrl);
+ errorUrl.searchParams.append("error", error.message);
+ errorUrl.searchParams.append("provider", provider);
+ errorUrl.searchParams.append("success", "false");
+
+ res.redirect(errorUrl.toString());
}
-
- const userData = await userResponse.json();
-
- // 3. 标准化用户数据(不同提供者返回的字段不同)
- let normalizedUser = {};
-
- if (provider === "github") {
- normalizedUser = {
- providerId: String(userData.id),
- email: userData.email,
- name: userData.name || userData.login,
- avatarUrl: userData.avatar_url,
- };
- } else if (provider === "zerocat") {
- normalizedUser = {
- providerId: userData.openid,
- email: userData.email_verified ? userData.email : null,
- name: userData.nickname || userData.username,
- avatarUrl: userData.avatar,
- };
- } else if (provider === "hly") {
- // 厚浪云(Logto)标准OIDC用户信息
- normalizedUser = {
- providerId: userData.sub,
- email: userData.email_verified ? userData.email : null,
- name: userData.name || userData.preferred_username || userData.nickname,
- avatarUrl: userData.picture,
- };
- } else if (provider === "stcn") {
- // STCN(Casdoor)标准OIDC用户信息
- normalizedUser = {
- providerId: userData.sub,
- email: userData.email_verified ? userData.email : userData.email || null,
- name: userData.name || userData.preferred_username || userData.nickname,
- avatarUrl: userData.picture,
- };
- }
-
- // 名称为空时,用邮箱@前部分回填(若邮箱可用)
- if ((!normalizedUser.name || normalizedUser.name.trim() === "") && normalizedUser.email) {
- const at = normalizedUser.email.indexOf("@");
- if (at > 0) {
- normalizedUser.name = normalizedUser.email.substring(0, at);
- }
- }
-
- // 4. 查找或创建账户
- let account = await prisma.account.findUnique({
- where: {
- provider_providerId: {
- provider,
- providerId: normalizedUser.providerId,
- },
- },
- });
-
- if (account) {
- // 更新账户信息
- account = await prisma.account.update({
- where: { id: account.id },
- data: {
- email: normalizedUser.email || account.email,
- name: normalizedUser.name || account.name,
- avatarUrl: normalizedUser.avatarUrl || account.avatarUrl,
- providerData: userData,
- //refreshToken: tokenData.refresh_token || account.refreshToken,
- updatedAt: new Date(),
- },
- });
- } else {
- // 创建新账户
- const accessToken = generateAccessToken();
- account = await prisma.account.create({
- data: {
- provider,
- providerId: normalizedUser.providerId,
- email: normalizedUser.email,
- name: normalizedUser.name,
- avatarUrl: normalizedUser.avatarUrl,
- providerData: userData,
- accessToken,
- //refreshToken: tokenData.refresh_token,
- },
- });
- }
-
- // 5. 生成令牌对(访问令牌 + 刷新令牌)
- const tokens = await generateTokenPair(account);
-
- // 6. 重定向到前端根路径,携带JWT token
- const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
- const callbackUrl = new URL(frontendBaseUrl);
- callbackUrl.searchParams.append("access_token", tokens.accessToken);
- callbackUrl.searchParams.append("refresh_token", tokens.refreshToken);
- callbackUrl.searchParams.append("expires_in", tokens.accessTokenExpiresIn);
- callbackUrl.searchParams.append("provider", provider);
- // 附带展示信息,便于前端显示品牌与名称
- const pconf = oauthProviders[provider] || {};
- callbackUrl.searchParams.append("providerName", pconf.displayName || pconf.name || provider);
- if (pconf.brandColor || pconf.color) {
- callbackUrl.searchParams.append("providerColor", pconf.brandColor || pconf.color);
- }
- callbackUrl.searchParams.append("success", "true");
-
- res.redirect(callbackUrl.toString());
-
- } catch (error) {
- console.error(`OAuth回调处理失败 [${provider}]:`, error);
-
- // 重定向到前端根路径,携带错误信息
- const frontendBaseUrl = process.env.FRONTEND_URL || "http://localhost:5173";
- const errorUrl = new URL(frontendBaseUrl);
- errorUrl.searchParams.append("error", error.message);
- errorUrl.searchParams.append("provider", provider);
- errorUrl.searchParams.append("success", "false");
-
- res.redirect(errorUrl.toString());
- }
});
/**
@@ -374,54 +374,54 @@ router.get("/oauth/:provider/callback", async (req, res) => {
* Authorization: Bearer
*/
router.get("/profile", jwtAuth, async (req, res, next) => {
- try {
- const accountContext = res.locals.account;
+ try {
+ const accountContext = res.locals.account;
- const account = await prisma.account.findUnique({
- where: { id: accountContext.id },
- include: {
- devices: {
- select: {
- id: true,
- uuid: true,
- name: true,
- createdAt: true,
- },
- },
- },
- });
+ const account = await prisma.account.findUnique({
+ where: {id: accountContext.id},
+ include: {
+ devices: {
+ select: {
+ id: true,
+ uuid: true,
+ name: true,
+ createdAt: true,
+ },
+ },
+ },
+ });
- // 组装 provider 展示信息
- const pconf = (account?.provider && oauthProviders[account.provider]) || {};
- const providerInfo = {
- id: account?.provider || undefined,
- name: pconf.name,
- displayName: pconf.displayName || pconf.name || account?.provider,
- icon: pconf.icon,
- color: pconf.color, // 兼容字段
- brandColor: pconf.brandColor || pconf.color,
- textColor: pconf.textColor || "#ffffff",
- description: pconf.description,
- order: typeof pconf.order === 'number' ? pconf.order : undefined,
- website: pconf.website,
- };
+ // 组装 provider 展示信息
+ const pconf = (account?.provider && oauthProviders[account.provider]) || {};
+ const providerInfo = {
+ id: account?.provider || undefined,
+ name: pconf.name,
+ displayName: pconf.displayName || pconf.name || account?.provider,
+ icon: pconf.icon,
+ color: pconf.color, // 兼容字段
+ brandColor: pconf.brandColor || pconf.color,
+ textColor: pconf.textColor || "#ffffff",
+ description: pconf.description,
+ order: typeof pconf.order === 'number' ? pconf.order : undefined,
+ website: pconf.website,
+ };
- res.json({
- success: true,
- data: {
- id: account.id,
- provider: account.provider,
- providerInfo,
- email: account.email,
- name: account.name,
- avatarUrl: account.avatarUrl,
- devices: account.devices,
- createdAt: account.createdAt,
- },
- });
- } catch (error) {
- next(error);
- }
+ res.json({
+ success: true,
+ data: {
+ id: account.id,
+ provider: account.provider,
+ providerInfo,
+ email: account.email,
+ name: account.name,
+ avatarUrl: account.avatarUrl,
+ devices: account.devices,
+ createdAt: account.createdAt,
+ },
+ });
+ } catch (error) {
+ next(error);
+ }
});
/**
@@ -437,57 +437,57 @@ router.get("/profile", jwtAuth, async (req, res, next) => {
* }
*/
router.post("/devices/bind", jwtAuth, async (req, res, next) => {
- try {
- const accountContext = res.locals.account;
- const { uuid } = req.body;
+ try {
+ const accountContext = res.locals.account;
+ const {uuid} = req.body;
- if (!uuid) {
- return res.status(400).json({
- success: false,
- message: "缺少设备UUID",
- });
+ if (!uuid) {
+ return res.status(400).json({
+ success: false,
+ message: "缺少设备UUID",
+ });
+ }
+
+ // 查找设备
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ });
+
+ if (!device) {
+ return res.status(404).json({
+ success: false,
+ message: "设备不存在 #1",
+ });
+ }
+
+ // 检查设备是否已绑定其他账户
+ if (device.accountId && device.accountId !== accountContext.id) {
+ return res.status(400).json({
+ success: false,
+ message: "设备已绑定其他账户",
+ });
+ }
+
+ // 绑定设备到账户
+ const updatedDevice = await prisma.device.update({
+ where: {uuid},
+ data: {
+ accountId: accountContext.id,
+ },
+ });
+
+ res.json({
+ success: true,
+ message: "设备绑定成功",
+ data: {
+ deviceId: updatedDevice.id,
+ uuid: updatedDevice.uuid,
+ name: updatedDevice.name,
+ },
+ });
+ } catch (error) {
+ next(error);
}
-
- // 查找设备
- const device = await prisma.device.findUnique({
- where: { uuid },
- });
-
- if (!device) {
- return res.status(404).json({
- success: false,
- message: "设备不存在 #1",
- });
- }
-
- // 检查设备是否已绑定其他账户
- if (device.accountId && device.accountId !== accountContext.id) {
- return res.status(400).json({
- success: false,
- message: "设备已绑定其他账户",
- });
- }
-
- // 绑定设备到账户
- const updatedDevice = await prisma.device.update({
- where: { uuid },
- data: {
- accountId: accountContext.id,
- },
- });
-
- res.json({
- success: true,
- message: "设备绑定成功",
- data: {
- deviceId: updatedDevice.id,
- uuid: updatedDevice.uuid,
- name: updatedDevice.name,
- },
- });
- } catch (error) {
- next(error);
- }
});
/**
@@ -504,65 +504,65 @@ router.post("/devices/bind", jwtAuth, async (req, res, next) => {
* }
*/
router.post("/devices/unbind", jwtAuth, async (req, res, next) => {
- try {
- const accountContext = res.locals.account;
- const { uuid, uuids } = req.body;
+ try {
+ const accountContext = res.locals.account;
+ const {uuid, uuids} = req.body;
- // 支持单个解绑或批量解绑
- const uuidsToUnbind = uuids || (uuid ? [uuid] : []);
+ // 支持单个解绑或批量解绑
+ const uuidsToUnbind = uuids || (uuid ? [uuid] : []);
- if (uuidsToUnbind.length === 0) {
- return res.status(400).json({
- success: false,
- message: "请提供要解绑的设备UUID",
- });
+ if (uuidsToUnbind.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: "请提供要解绑的设备UUID",
+ });
+ }
+
+ // 查找所有设备并验证所有权
+ const devices = await prisma.device.findMany({
+ where: {
+ uuid: {in: uuidsToUnbind},
+ },
+ });
+
+ // 检查是否有不存在的设备
+ if (devices.length !== uuidsToUnbind.length) {
+ const foundUuids = devices.map(d => d.uuid);
+ const notFoundUuids = uuidsToUnbind.filter(u => !foundUuids.includes(u));
+ return res.status(404).json({
+ success: false,
+ message: `以下设备不存在: ${notFoundUuids.join(', ')}`,
+ });
+ }
+
+ // 检查所有权
+ const unauthorizedDevices = devices.filter(d => d.accountId !== accountContext.id);
+ if (unauthorizedDevices.length > 0) {
+ return res.status(403).json({
+ success: false,
+ message: `您没有权限解绑以下设备: ${unauthorizedDevices.map(d => d.uuid).join(', ')}`,
+ });
+ }
+
+ // 批量解绑设备
+ await prisma.device.updateMany({
+ where: {
+ uuid: {in: uuidsToUnbind},
+ accountId: accountContext.id,
+ },
+ data: {
+ accountId: null,
+ },
+ });
+
+ res.json({
+ success: true,
+ message: uuidsToUnbind.length === 1 ? "设备解绑成功" : `成功解绑 ${uuidsToUnbind.length} 个设备`,
+ unboundCount: uuidsToUnbind.length,
+ });
+ } catch (error) {
+ next(error);
}
-
- // 查找所有设备并验证所有权
- const devices = await prisma.device.findMany({
- where: {
- uuid: { in: uuidsToUnbind },
- },
- });
-
- // 检查是否有不存在的设备
- if (devices.length !== uuidsToUnbind.length) {
- const foundUuids = devices.map(d => d.uuid);
- const notFoundUuids = uuidsToUnbind.filter(u => !foundUuids.includes(u));
- return res.status(404).json({
- success: false,
- message: `以下设备不存在: ${notFoundUuids.join(', ')}`,
- });
- }
-
- // 检查所有权
- const unauthorizedDevices = devices.filter(d => d.accountId !== accountContext.id);
- if (unauthorizedDevices.length > 0) {
- return res.status(403).json({
- success: false,
- message: `您没有权限解绑以下设备: ${unauthorizedDevices.map(d => d.uuid).join(', ')}`,
- });
- }
-
- // 批量解绑设备
- await prisma.device.updateMany({
- where: {
- uuid: { in: uuidsToUnbind },
- accountId: accountContext.id,
- },
- data: {
- accountId: null,
- },
- });
-
- res.json({
- success: true,
- message: uuidsToUnbind.length === 1 ? "设备解绑成功" : `成功解绑 ${uuidsToUnbind.length} 个设备`,
- unboundCount: uuidsToUnbind.length,
- });
- } catch (error) {
- next(error);
- }
});
/**
@@ -573,32 +573,32 @@ router.post("/devices/unbind", jwtAuth, async (req, res, next) => {
* Authorization: Bearer
*/
router.get("/devices", jwtAuth, async (req, res, next) => {
- try {
- const accountContext = res.locals.account;
- // 获取账户的设备列表
- const account = await prisma.account.findUnique({
- where: { id: accountContext.id },
- include: {
- devices: {
- select: {
- id: true,
- uuid: true,
- name: true,
- namespace: true,
- createdAt: true,
- updatedAt: true,
- },
- },
- },
- });
+ try {
+ const accountContext = res.locals.account;
+ // 获取账户的设备列表
+ const account = await prisma.account.findUnique({
+ where: {id: accountContext.id},
+ include: {
+ devices: {
+ select: {
+ id: true,
+ uuid: true,
+ name: true,
+ namespace: true,
+ createdAt: true,
+ updatedAt: true,
+ },
+ },
+ },
+ });
- res.json({
- success: true,
- data: account.devices,
- });
- } catch (error) {
- next(error);
- }
+ res.json({
+ success: true,
+ data: account.devices,
+ });
+ } catch (error) {
+ next(error);
+ }
});
/**
@@ -608,52 +608,52 @@ router.get("/devices", jwtAuth, async (req, res, next) => {
* 无需认证,返回公开信息
*/
router.get("/device/:uuid/account", async (req, res, next) => {
- try {
- const { uuid } = req.params;
+ try {
+ const {uuid} = req.params;
- // 查找设备及其关联的账户
- const device = await prisma.device.findUnique({
- where: { uuid },
- include: {
- account: {
- select: {
- id: true,
- provider: true,
- name: true,
- avatarUrl: true,
- createdAt: true,
- },
- },
- },
- });
+ // 查找设备及其关联的账户
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ include: {
+ account: {
+ select: {
+ id: true,
+ provider: true,
+ name: true,
+ avatarUrl: true,
+ createdAt: true,
+ },
+ },
+ },
+ });
- if (!device) {
- return res.status(404).json({
- success: false,
- message: "设备不存在 #2",
- });
+ if (!device) {
+ return res.status(404).json({
+ success: false,
+ message: "设备不存在 #2",
+ });
+ }
+
+ if (!device.account) {
+ return res.json({
+ success: true,
+ data: null, // 设备未绑定账户
+ });
+ }
+
+ res.json({
+ success: true,
+ data: {
+ id: device.account.id,
+ provider: device.account.provider,
+ name: device.account.name,
+ avatarUrl: device.account.avatarUrl,
+ bindTime: device.updatedAt, // 绑定时间
+ },
+ });
+ } catch (error) {
+ next(error);
}
-
- if (!device.account) {
- return res.json({
- success: true,
- data: null, // 设备未绑定账户
- });
- }
-
- res.json({
- success: true,
- data: {
- id: device.account.id,
- provider: device.account.provider,
- name: device.account.name,
- avatarUrl: device.account.avatarUrl,
- bindTime: device.updatedAt, // 绑定时间
- },
- });
- } catch (error) {
- next(error);
- }
});
/**
@@ -666,44 +666,44 @@ router.get("/device/:uuid/account", async (req, res, next) => {
* }
*/
router.post("/refresh", async (req, res, next) => {
- try {
- const { refresh_token } = req.body;
+ try {
+ const {refresh_token} = req.body;
- if (!refresh_token) {
- return res.status(400).json({
- success: false,
- message: "缺少刷新令牌",
- });
- }
+ if (!refresh_token) {
+ return res.status(400).json({
+ success: false,
+ message: "缺少刷新令牌",
+ });
+ }
- // 刷新访问令牌
- const result = await refreshAccessToken(refresh_token);
+ // 刷新访问令牌
+ const result = await refreshAccessToken(refresh_token);
- res.json({
- success: true,
- message: "令牌刷新成功",
- data: {
- access_token: result.accessToken,
- expires_in: result.accessTokenExpiresIn,
- account: result.account,
- },
- });
- } catch (error) {
- if (error.message === 'Account not found') {
- return next(errors.createError(401, "账户不存在"));
- }
- if (error.message === 'Invalid refresh token') {
- return next(errors.createError(401, "无效的刷新令牌"));
- }
- if (error.message === 'Refresh token expired') {
- return next(errors.createError(401, "刷新令牌已过期"));
- }
- if (error.message === 'Token version mismatch') {
- return next(errors.createError(401, "令牌版本不匹配,请重新登录"));
- }
+ res.json({
+ success: true,
+ message: "令牌刷新成功",
+ data: {
+ access_token: result.accessToken,
+ expires_in: result.accessTokenExpiresIn,
+ account: result.account,
+ },
+ });
+ } catch (error) {
+ if (error.message === 'Account not found') {
+ return next(errors.createError(401, "账户不存在"));
+ }
+ if (error.message === 'Invalid refresh token') {
+ return next(errors.createError(401, "无效的刷新令牌"));
+ }
+ if (error.message === 'Refresh token expired') {
+ return next(errors.createError(401, "刷新令牌已过期"));
+ }
+ if (error.message === 'Token version mismatch') {
+ return next(errors.createError(401, "令牌版本不匹配,请重新登录"));
+ }
- next(error);
- }
+ next(error);
+ }
});
/**
@@ -714,19 +714,19 @@ router.post("/refresh", async (req, res, next) => {
* Authorization: Bearer
*/
router.post("/logout", jwtAuth, async (req, res, next) => {
- try {
- const accountContext = res.locals.account;
+ try {
+ const accountContext = res.locals.account;
- // 撤销当前设备的刷新令牌
- await revokeRefreshToken(accountContext.id);
+ // 撤销当前设备的刷新令牌
+ await revokeRefreshToken(accountContext.id);
- res.json({
- success: true,
- message: "登出成功",
- });
- } catch (error) {
- next(error);
- }
+ res.json({
+ success: true,
+ message: "登出成功",
+ });
+ } catch (error) {
+ next(error);
+ }
});
/**
@@ -737,19 +737,19 @@ router.post("/logout", jwtAuth, async (req, res, next) => {
* Authorization: Bearer
*/
router.post("/logout-all", jwtAuth, async (req, res, next) => {
- try {
- const accountContext = res.locals.account;
+ try {
+ const accountContext = res.locals.account;
- // 撤销所有令牌
- await revokeAllTokens(accountContext.id);
+ // 撤销所有令牌
+ await revokeAllTokens(accountContext.id);
- res.json({
- success: true,
- message: "已从所有设备登出",
- });
- } catch (error) {
- next(error);
- }
+ res.json({
+ success: true,
+ message: "已从所有设备登出",
+ });
+ } catch (error) {
+ next(error);
+ }
});
/**
@@ -760,32 +760,32 @@ router.post("/logout-all", jwtAuth, async (req, res, next) => {
* Authorization: Bearer
*/
router.get("/token-info", jwtAuth, async (req, res, next) => {
- try {
- const decoded = res.locals.tokenDecoded;
- const account = res.locals.account;
+ try {
+ const decoded = res.locals.tokenDecoded;
+ const account = res.locals.account;
- // 计算token剩余有效时间
- const now = Math.floor(Date.now() / 1000);
- const expiresIn = decoded.exp - now;
+ // 计算token剩余有效时间
+ const now = Math.floor(Date.now() / 1000);
+ const expiresIn = decoded.exp - now;
- res.json({
- success: true,
- data: {
- accountId: account.id,
- tokenType: decoded.type || 'legacy',
- tokenVersion: decoded.tokenVersion || account.tokenVersion,
- issuedAt: new Date(decoded.iat * 1000),
- expiresAt: new Date(decoded.exp * 1000),
- expiresIn: expiresIn,
- isExpired: expiresIn <= 0,
- isLegacyToken: res.locals.isLegacyToken || false,
- hasRefreshToken: !!account.refreshToken,
- refreshTokenExpiry: account.refreshTokenExpiry,
- },
- });
- } catch (error) {
- next(error);
- }
+ res.json({
+ success: true,
+ data: {
+ accountId: account.id,
+ tokenType: decoded.type || 'legacy',
+ tokenVersion: decoded.tokenVersion || account.tokenVersion,
+ issuedAt: new Date(decoded.iat * 1000),
+ expiresAt: new Date(decoded.exp * 1000),
+ expiresIn: expiresIn,
+ isExpired: expiresIn <= 0,
+ isLegacyToken: res.locals.isLegacyToken || false,
+ hasRefreshToken: !!account.refreshToken,
+ refreshTokenExpiry: account.refreshTokenExpiry,
+ },
+ });
+ } catch (error) {
+ next(error);
+ }
});
export default router;
\ No newline at end of file
diff --git a/routes/apps.js b/routes/apps.js
index f6a6465..be79535 100644
--- a/routes/apps.js
+++ b/routes/apps.js
@@ -1,12 +1,11 @@
-import { Router } from "express";
-const router = Router();
-import { uuidAuth } from "../middleware/uuidAuth.js";
-import { jwtAuth } from "../middleware/jwt-auth.js";
-import { kvTokenAuth } from "../middleware/kvTokenAuth.js";
-import { PrismaClient } from "@prisma/client";
+import {Router} from "express";
+import {uuidAuth} from "../middleware/uuidAuth.js";
+import {PrismaClient} from "@prisma/client";
import crypto from "crypto";
import errors from "../utils/errors.js";
-import { verifyDevicePassword } from "../utils/crypto.js";
+import {verifyDevicePassword} from "../utils/crypto.js";
+
+const router = Router();
const prisma = new PrismaClient();
@@ -15,35 +14,35 @@ const prisma = new PrismaClient();
* 获取设备安装的应用列表 (公开接口,无需认证)
*/
router.get(
- "/devices/:uuid/apps",
- errors.catchAsync(async (req, res, next) => {
- const { uuid } = req.params;
+ "/devices/:uuid/apps",
+ errors.catchAsync(async (req, res, next) => {
+ const {uuid} = req.params;
- // 查找设备
- const device = await prisma.device.findUnique({
- where: { uuid },
- });
+ // 查找设备
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ });
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
+ }
- const installations = await prisma.appInstall.findMany({
- where: { deviceId: device.id },
- });
+ const installations = await prisma.appInstall.findMany({
+ where: {deviceId: device.id},
+ });
- const apps = installations.map(install => ({
- appId: install.appId,
- token: install.token,
- note: install.note,
- installedAt: install.createdAt,
- }));
+ const apps = installations.map(install => ({
+ appId: install.appId,
+ token: install.token,
+ note: install.note,
+ installedAt: install.createdAt,
+ }));
- return res.json({
- success: true,
- apps,
- });
- })
+ return res.json({
+ success: true,
+ apps,
+ });
+ })
);
/**
@@ -52,35 +51,35 @@ router.get(
* appId 现在是 SHA256 hash
*/
router.post(
- "/devices/:uuid/install/:appId",
- uuidAuth,
- errors.catchAsync(async (req, res, next) => {
- const device = res.locals.device;
- const { appId } = req.params;
- const { note } = req.body;
+ "/devices/:uuid/install/:appId",
+ uuidAuth,
+ errors.catchAsync(async (req, res, next) => {
+ const device = res.locals.device;
+ const {appId} = req.params;
+ const {note} = req.body;
- // 生成token
- const token = crypto.randomBytes(32).toString("hex");
+ // 生成token
+ const token = crypto.randomBytes(32).toString("hex");
- // 创建安装记录
- const installation = await prisma.appInstall.create({
- data: {
- deviceId: device.id,
- appId: appId,
- token,
- note: note || null,
- },
- });
+ // 创建安装记录
+ const installation = await prisma.appInstall.create({
+ data: {
+ deviceId: device.id,
+ appId: appId,
+ token,
+ note: note || null,
+ },
+ });
- return res.status(201).json({
- id: installation.id,
- appId: installation.appId,
- token: installation.token,
- note: installation.note,
- name: installation.note, // 备注同时作为名称返回
- installedAt: installation.createdAt,
- });
- })
+ return res.status(201).json({
+ id: installation.id,
+ appId: installation.appId,
+ token: installation.token,
+ note: installation.note,
+ name: installation.note, // 备注同时作为名称返回
+ installedAt: installation.createdAt,
+ });
+ })
);
/**
@@ -88,31 +87,31 @@ router.post(
* 卸载设备应用 (需要UUID认证)
*/
router.delete(
- "/devices/:uuid/uninstall/:installId",
- uuidAuth,
- errors.catchAsync(async (req, res, next) => {
- const device = res.locals.device;
- const { installId } = req.params;
+ "/devices/:uuid/uninstall/:installId",
+ uuidAuth,
+ errors.catchAsync(async (req, res, next) => {
+ const device = res.locals.device;
+ const {installId} = req.params;
- const installation = await prisma.appInstall.findUnique({
- where: { id: installId },
- });
+ const installation = await prisma.appInstall.findUnique({
+ where: {id: installId},
+ });
- if (!installation) {
- return next(errors.createError(404, "应用未安装"));
- }
+ if (!installation) {
+ return next(errors.createError(404, "应用未安装"));
+ }
- // 确保安装记录属于当前设备
- if (installation.deviceId !== device.id) {
- return next(errors.createError(403, "无权操作此安装记录"));
- }
+ // 确保安装记录属于当前设备
+ if (installation.deviceId !== device.id) {
+ return next(errors.createError(403, "无权操作此安装记录"));
+ }
- await prisma.appInstall.delete({
- where: { id: installation.id },
- });
+ await prisma.appInstall.delete({
+ where: {id: installation.id},
+ });
- return res.status(204).end();
- })
+ return res.status(204).end();
+ })
);
/**
@@ -120,44 +119,44 @@ router.delete(
* 获取设备的token列表 (需要设备UUID)
*/
router.get(
- "/tokens",
- errors.catchAsync(async (req, res, next) => {
- const { uuid } = req.query;
+ "/tokens",
+ errors.catchAsync(async (req, res, next) => {
+ const {uuid} = req.query;
- if (!uuid) {
- return next(errors.createError(400, "需要提供设备UUID"));
- }
+ if (!uuid) {
+ return next(errors.createError(400, "需要提供设备UUID"));
+ }
- // 查找设备
- const device = await prisma.device.findUnique({
- where: { uuid },
- });
+ // 查找设备
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ });
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
+ }
- // 获取该设备的所有应用安装记录(即token)
- const installations = await prisma.appInstall.findMany({
- where: { deviceId: device.id },
- orderBy: { installedAt: 'desc' },
- });
+ // 获取该设备的所有应用安装记录(即token)
+ const installations = await prisma.appInstall.findMany({
+ where: {deviceId: device.id},
+ orderBy: {installedAt: 'desc'},
+ });
- const tokens = installations.map(install => ({
- id: install.id,
- token: install.token,
- appId: install.appId,
- installedAt: install.installedAt,
- note: install.note,
- name: install.note, // 备注同时作为名称返回
- }));
+ const tokens = installations.map(install => ({
+ id: install.id,
+ token: install.token,
+ appId: install.appId,
+ installedAt: install.installedAt,
+ note: install.note,
+ name: install.note, // 备注同时作为名称返回
+ }));
- return res.json({
- success: true,
- tokens,
- deviceUuid: uuid,
- });
- })
+ return res.json({
+ success: true,
+ tokens,
+ deviceUuid: uuid,
+ });
+ })
);
/**
@@ -166,97 +165,97 @@ router.get(
* Body: { namespace: string, password: string, appId: string }
*/
router.post(
- "/auth/token",
- errors.catchAsync(async (req, res, next) => {
- const { namespace, password, appId } = req.body;
+ "/auth/token",
+ errors.catchAsync(async (req, res, next) => {
+ const {namespace, password, appId} = req.body;
- if (!namespace) {
- return next(errors.createError(400, "需要提供 namespace"));
- }
-
- if (!appId) {
- return next(errors.createError(400, "需要提供 appId"));
- }
-
- // 通过 namespace 查找设备
- const device = await prisma.device.findUnique({
- where: { namespace },
- include: {
- autoAuths: true,
- },
- });
-
- if (!device) {
- return next(errors.createError(404, "设备不存在或 namespace 不正确"));
- }
-
- // 查找匹配的自动授权配置
- let matchedAutoAuth = null;
-
- // 如果提供了密码,查找匹配密码的自动授权
- if (password) {
- // 首先尝试直接匹配明文密码
- matchedAutoAuth = device.autoAuths.find(auth => auth.password === password);
-
- // 如果没有匹配到,尝试验证哈希密码(向后兼容)
- if (!matchedAutoAuth) {
- for (const autoAuth of device.autoAuths) {
- if (autoAuth.password && autoAuth.password.startsWith('$2')) { // bcrypt 哈希以 $2 开头
- try {
- if (await verifyDevicePassword(password, autoAuth.password)) {
- matchedAutoAuth = autoAuth;
-
- // 自动迁移:将哈希密码更新为明文密码
- await prisma.autoAuth.update({
- where: { id: autoAuth.id },
- data: { password: password }, // 保存明文密码
- });
-
- console.log(`AutoAuth ${autoAuth.id} 密码已自动迁移为明文`);
- break;
- }
- } catch (err) {
- // 如果验证失败,继续尝试下一个
- continue;
- }
- }
+ if (!namespace) {
+ return next(errors.createError(400, "需要提供 namespace"));
}
- }
- if (!matchedAutoAuth) {
- return next(errors.createError(401, "密码不正确"));
- }
- } else {
- // 如果没有提供密码,查找密码为空的自动授权
- matchedAutoAuth = device.autoAuths.find(auth => !auth.password);
+ if (!appId) {
+ return next(errors.createError(400, "需要提供 appId"));
+ }
- if (!matchedAutoAuth) {
- return next(errors.createError(401, "需要提供密码"));
- }
- }
+ // 通过 namespace 查找设备
+ const device = await prisma.device.findUnique({
+ where: {namespace},
+ include: {
+ autoAuths: true,
+ },
+ });
- // 根据自动授权配置创建 AppInstall
- const token = crypto.randomBytes(32).toString("hex");
+ if (!device) {
+ return next(errors.createError(404, "设备不存在或 namespace 不正确"));
+ }
- const installation = await prisma.appInstall.create({
- data: {
- deviceId: device.id,
- appId: appId,
- token,
- note: null,
- isReadOnly: matchedAutoAuth.isReadOnly,
- deviceType: matchedAutoAuth.deviceType,
- },
- });
+ // 查找匹配的自动授权配置
+ let matchedAutoAuth = null;
- return res.status(201).json({
- success: true,
- token: installation.token,
- deviceType: installation.deviceType,
- isReadOnly: installation.isReadOnly,
- installedAt: installation.installedAt,
- });
- })
+ // 如果提供了密码,查找匹配密码的自动授权
+ if (password) {
+ // 首先尝试直接匹配明文密码
+ matchedAutoAuth = device.autoAuths.find(auth => auth.password === password);
+
+ // 如果没有匹配到,尝试验证哈希密码(向后兼容)
+ if (!matchedAutoAuth) {
+ for (const autoAuth of device.autoAuths) {
+ if (autoAuth.password && autoAuth.password.startsWith('$2')) { // bcrypt 哈希以 $2 开头
+ try {
+ if (await verifyDevicePassword(password, autoAuth.password)) {
+ matchedAutoAuth = autoAuth;
+
+ // 自动迁移:将哈希密码更新为明文密码
+ await prisma.autoAuth.update({
+ where: {id: autoAuth.id},
+ data: {password: password}, // 保存明文密码
+ });
+
+ console.log(`AutoAuth ${autoAuth.id} 密码已自动迁移为明文`);
+ break;
+ }
+ } catch (err) {
+ // 如果验证失败,继续尝试下一个
+
+ }
+ }
+ }
+ }
+
+ if (!matchedAutoAuth) {
+ return next(errors.createError(401, "密码不正确"));
+ }
+ } else {
+ // 如果没有提供密码,查找密码为空的自动授权
+ matchedAutoAuth = device.autoAuths.find(auth => !auth.password);
+
+ if (!matchedAutoAuth) {
+ return next(errors.createError(401, "需要提供密码"));
+ }
+ }
+
+ // 根据自动授权配置创建 AppInstall
+ const token = crypto.randomBytes(32).toString("hex");
+
+ const installation = await prisma.appInstall.create({
+ data: {
+ deviceId: device.id,
+ appId: appId,
+ token,
+ note: null,
+ isReadOnly: matchedAutoAuth.isReadOnly,
+ deviceType: matchedAutoAuth.deviceType,
+ },
+ });
+
+ return res.status(201).json({
+ success: true,
+ token: installation.token,
+ deviceType: installation.deviceType,
+ isReadOnly: installation.isReadOnly,
+ installedAt: installation.installedAt,
+ });
+ })
);
/**
@@ -265,78 +264,78 @@ router.post(
* Body: { name: string }
*/
router.post(
- "/tokens/:token/set-student-name",
- errors.catchAsync(async (req, res, next) => {
- const { token } = req.params;
- const { name } = req.body;
+ "/tokens/:token/set-student-name",
+ errors.catchAsync(async (req, res, next) => {
+ const {token} = req.params;
+ const {name} = req.body;
- if (!name) {
- return next(errors.createError(400, "需要提供学生名称"));
- }
+ if (!name) {
+ return next(errors.createError(400, "需要提供学生名称"));
+ }
- // 查找 token 对应的应用安装记录
- const appInstall = await prisma.appInstall.findUnique({
- where: { token },
- include: {
- device: true,
- },
- });
+ // 查找 token 对应的应用安装记录
+ const appInstall = await prisma.appInstall.findUnique({
+ where: {token},
+ include: {
+ device: true,
+ },
+ });
- if (!appInstall) {
- return next(errors.createError(404, "Token 不存在"));
- }
+ if (!appInstall) {
+ return next(errors.createError(404, "Token 不存在"));
+ }
- // 验证 token 类型是否为 student
- if (!['student','parent'].includes(appInstall.deviceType)) {
- return next(errors.createError(403, "只有学生和家长类型的 token 可以设置名称"));
- }
+ // 验证 token 类型是否为 student
+ if (!['student', 'parent'].includes(appInstall.deviceType)) {
+ return next(errors.createError(403, "只有学生和家长类型的 token 可以设置名称"));
+ }
- // 读取设备的 classworks-list-main 键值
- const kvRecord = await prisma.kVStore.findUnique({
- where: {
- deviceId_key: {
- deviceId: appInstall.deviceId,
- key: 'classworks-list-main',
- },
- },
- });
+ // 读取设备的 classworks-list-main 键值
+ const kvRecord = await prisma.kVStore.findUnique({
+ where: {
+ deviceId_key: {
+ deviceId: appInstall.deviceId,
+ key: 'classworks-list-main',
+ },
+ },
+ });
- if (!kvRecord) {
- return next(errors.createError(404, "设备未设置学生列表"));
- }
+ if (!kvRecord) {
+ return next(errors.createError(404, "设备未设置学生列表"));
+ }
- // 解析学生列表
- let studentList;
- try {
- studentList = kvRecord.value;
- if (!Array.isArray(studentList)) {
- return next(errors.createError(500, "学生列表格式错误"));
- }
- } catch (error) {
- return next(errors.createError(500, "无法解析学生列表"));
- }
+ // 解析学生列表
+ let studentList;
+ try {
+ studentList = kvRecord.value;
+ if (!Array.isArray(studentList)) {
+ return next(errors.createError(500, "学生列表格式错误"));
+ }
+ } catch (error) {
+ return next(errors.createError(500, "无法解析学生列表"));
+ }
- // 验证名称是否在学生列表中
- const studentExists = studentList.some(student => student.name === name);
+ // 验证名称是否在学生列表中
+ const studentExists = studentList.some(student => student.name === name);
- if (!studentExists) {
- return next(errors.createError(400, "该名称不在学生列表中"));
- }
+ if (!studentExists) {
+ return next(errors.createError(400, "该名称不在学生列表中"));
+ }
- // 更新 AppInstall 的 note 字段
- const updatedInstall = await prisma.appInstall.update({
- where: { id: appInstall.id },
- data: { note: appInstall.deviceType === 'parent' ? `${name} 家长` : name },
- });
+ // 更新 AppInstall 的 note 字段
+ const updatedInstall = await prisma.appInstall.update({
+ where: {id: appInstall.id},
+ data: {note: appInstall.deviceType === 'parent' ? `${name} 家长` : name},
+ });
- return res.json({
- success: true,
- token: updatedInstall.token,
- name: updatedInstall.note,
- deviceType: updatedInstall.deviceType,
- updatedAt: updatedInstall.updatedAt,
- });
- })
+ return res.json({
+ success: true,
+ token: updatedInstall.token,
+ name: updatedInstall.note,
+ deviceType: updatedInstall.deviceType,
+ updatedAt: updatedInstall.updatedAt,
+ });
+ })
);
/**
@@ -345,33 +344,33 @@ router.post(
* Body: { note: string }
*/
router.put(
- "/tokens/:token/note",
- errors.catchAsync(async (req, res, next) => {
- const { token } = req.params;
- const { note } = req.body;
+ "/tokens/:token/note",
+ errors.catchAsync(async (req, res, next) => {
+ const {token} = req.params;
+ const {note} = req.body;
- // 查找 token 对应的应用安装记录
- const appInstall = await prisma.appInstall.findUnique({
- where: { token },
- });
+ // 查找 token 对应的应用安装记录
+ const appInstall = await prisma.appInstall.findUnique({
+ where: {token},
+ });
- if (!appInstall) {
- return next(errors.createError(404, "Token 不存在"));
- }
+ if (!appInstall) {
+ return next(errors.createError(404, "Token 不存在"));
+ }
- // 更新 AppInstall 的 note 字段
- const updatedInstall = await prisma.appInstall.update({
- where: { id: appInstall.id },
- data: { note: note || null },
- });
+ // 更新 AppInstall 的 note 字段
+ const updatedInstall = await prisma.appInstall.update({
+ where: {id: appInstall.id},
+ data: {note: note || null},
+ });
- return res.json({
- success: true,
- token: updatedInstall.token,
- note: updatedInstall.note,
- updatedAt: updatedInstall.updatedAt,
- });
- })
+ return res.json({
+ success: true,
+ token: updatedInstall.token,
+ note: updatedInstall.note,
+ updatedAt: updatedInstall.updatedAt,
+ });
+ })
);
export default router;
\ No newline at end of file
diff --git a/routes/auto-auth.js b/routes/auto-auth.js
index 68b74ed..a16d490 100644
--- a/routes/auto-auth.js
+++ b/routes/auto-auth.js
@@ -1,9 +1,10 @@
-import { Router } from "express";
-const router = Router();
-import { jwtAuth } from "../middleware/jwt-auth.js";
-import { PrismaClient } from "@prisma/client";
+import {Router} from "express";
+import {jwtAuth} from "../middleware/jwt-auth.js";
+import {PrismaClient} from "@prisma/client";
import errors from "../utils/errors.js";
+const router = Router();
+
const prisma = new PrismaClient();
/**
@@ -11,52 +12,52 @@ const prisma = new PrismaClient();
* 获取设备的所有自动授权配置 (需要 JWT 认证,且设备必须绑定到该账户)
*/
router.get(
- "/devices/:uuid/auth-configs",
- jwtAuth,
- errors.catchAsync(async (req, res, next) => {
- const { uuid } = req.params;
- const account = res.locals.account;
+ "/devices/:uuid/auth-configs",
+ jwtAuth,
+ errors.catchAsync(async (req, res, next) => {
+ const {uuid} = req.params;
+ const account = res.locals.account;
- // 查找设备并验证是否属于当前账户
- const device = await prisma.device.findUnique({
- where: { uuid },
- });
+ // 查找设备并验证是否属于当前账户
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ });
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
+ }
- // 验证设备是否绑定到当前账户
- if (!device.accountId || device.accountId !== account.id) {
- return next(errors.createError(403, "该设备未绑定到您的账户"));
- }
+ // 验证设备是否绑定到当前账户
+ if (!device.accountId || device.accountId !== account.id) {
+ return next(errors.createError(403, "该设备未绑定到您的账户"));
+ }
- const autoAuths = await prisma.autoAuth.findMany({
- where: { deviceId: device.id },
- orderBy: { createdAt: 'desc' },
- });
+ const autoAuths = await prisma.autoAuth.findMany({
+ where: {deviceId: device.id},
+ orderBy: {createdAt: 'desc'},
+ });
- // 返回配置,智能处理密码显示
- const configs = autoAuths.map(auth => {
- // 检查是否是 bcrypt 哈希密码
- const isHashedPassword = auth.password && auth.password.startsWith('$2');
+ // 返回配置,智能处理密码显示
+ const configs = autoAuths.map(auth => {
+ // 检查是否是 bcrypt 哈希密码
+ const isHashedPassword = auth.password && auth.password.startsWith('$2');
- return {
- id: auth.id,
- password: isHashedPassword ? null : auth.password, // 哈希密码不返回
- isLegacyHash: isHashedPassword, // 标记是否为旧的哈希密码
- deviceType: auth.deviceType,
- isReadOnly: auth.isReadOnly,
- createdAt: auth.createdAt,
- updatedAt: auth.updatedAt,
- };
- });
+ return {
+ id: auth.id,
+ password: isHashedPassword ? null : auth.password, // 哈希密码不返回
+ isLegacyHash: isHashedPassword, // 标记是否为旧的哈希密码
+ deviceType: auth.deviceType,
+ isReadOnly: auth.isReadOnly,
+ createdAt: auth.createdAt,
+ updatedAt: auth.updatedAt,
+ };
+ });
- return res.json({
- success: true,
- configs,
- });
- })
+ return res.json({
+ success: true,
+ configs,
+ });
+ })
);
/**
@@ -65,163 +66,164 @@ router.get(
* Body: { password?: string, deviceType?: string, isReadOnly?: boolean }
*/
router.post(
- "/devices/:uuid/auth-configs",
- jwtAuth,
- errors.catchAsync(async (req, res, next) => {
- const { uuid } = req.params;
- const account = res.locals.account;
- const { password, deviceType, isReadOnly } = req.body;
+ "/devices/:uuid/auth-configs",
+ jwtAuth,
+ errors.catchAsync(async (req, res, next) => {
+ const {uuid} = req.params;
+ const account = res.locals.account;
+ const {password, deviceType, isReadOnly} = req.body;
- // 查找设备并验证是否属于当前账户
- const device = await prisma.device.findUnique({
- where: { uuid },
- });
+ // 查找设备并验证是否属于当前账户
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ });
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
+ }
- // 验证设备是否绑定到当前账户
- if (!device.accountId || device.accountId !== account.id) {
- return next(errors.createError(403, "该设备未绑定到您的账户"));
- }
+ // 验证设备是否绑定到当前账户
+ if (!device.accountId || device.accountId !== account.id) {
+ return next(errors.createError(403, "该设备未绑定到您的账户"));
+ }
- // 验证 deviceType 如果提供的话
- const validDeviceTypes = ['teacher', 'student', 'classroom', 'parent'];
- if (deviceType && !validDeviceTypes.includes(deviceType)) {
- return next(errors.createError(400, `设备类型必须是以下之一: ${validDeviceTypes.join(', ')}`));
- }
+ // 验证 deviceType 如果提供的话
+ const validDeviceTypes = ['teacher', 'student', 'classroom', 'parent'];
+ if (deviceType && !validDeviceTypes.includes(deviceType)) {
+ return next(errors.createError(400, `设备类型必须是以下之一: ${validDeviceTypes.join(', ')}`));
+ }
- // 规范化密码:空字符串视为 null
- const plainPassword = (password !== undefined && password !== '') ? password : null;
+ // 规范化密码:空字符串视为 null
+ const plainPassword = (password !== undefined && password !== '') ? password : null;
- // 查询该设备的所有自动授权配置,本地检查是否存在相同密码
- const allAuths = await prisma.autoAuth.findMany({
- where: { deviceId: device.id },
- });
+ // 查询该设备的所有自动授权配置,本地检查是否存在相同密码
+ const allAuths = await prisma.autoAuth.findMany({
+ where: {deviceId: device.id},
+ });
- const existingAuth = allAuths.find(auth => auth.password === plainPassword);
+ const existingAuth = allAuths.find(auth => auth.password === plainPassword);
- if (existingAuth) {
- return next(errors.createError(400, "该密码的自动授权配置已存在"));
- }
+ if (existingAuth) {
+ return next(errors.createError(400, "该密码的自动授权配置已存在"));
+ }
- // 创建新的自动授权配置(密码明文存储)
- const autoAuth = await prisma.autoAuth.create({
- data: {
- deviceId: device.id,
- password: plainPassword,
- deviceType: deviceType || null,
- isReadOnly: isReadOnly || false,
- },
- });
+ // 创建新的自动授权配置(密码明文存储)
+ const autoAuth = await prisma.autoAuth.create({
+ data: {
+ deviceId: device.id,
+ password: plainPassword,
+ deviceType: deviceType || null,
+ isReadOnly: isReadOnly || false,
+ },
+ });
- return res.status(201).json({
- success: true,
- config: {
- id: autoAuth.id,
- password: autoAuth.password, // 返回明文密码
- deviceType: autoAuth.deviceType,
- isReadOnly: autoAuth.isReadOnly,
- createdAt: autoAuth.createdAt,
- },
- });
- })
-);/**
+ return res.status(201).json({
+ success: true,
+ config: {
+ id: autoAuth.id,
+ password: autoAuth.password, // 返回明文密码
+ deviceType: autoAuth.deviceType,
+ isReadOnly: autoAuth.isReadOnly,
+ createdAt: autoAuth.createdAt,
+ },
+ });
+ })
+);
+/**
* PUT /auto-auth/devices/:uuid/auth-configs/:configId
* 更新自动授权配置 (需要 JWT 认证,且设备必须绑定到该账户)
* Body: { password?: string, deviceType?: string, isReadOnly?: boolean }
*/
router.put(
- "/devices/:uuid/auth-configs/:configId",
- jwtAuth,
- errors.catchAsync(async (req, res, next) => {
- const { uuid, configId } = req.params;
- const account = res.locals.account;
- const { password, deviceType, isReadOnly } = req.body;
+ "/devices/:uuid/auth-configs/:configId",
+ jwtAuth,
+ errors.catchAsync(async (req, res, next) => {
+ const {uuid, configId} = req.params;
+ const account = res.locals.account;
+ const {password, deviceType, isReadOnly} = req.body;
- // 查找设备并验证是否属于当前账户
- const device = await prisma.device.findUnique({
- where: { uuid },
- });
+ // 查找设备并验证是否属于当前账户
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ });
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
+ }
- // 验证设备是否绑定到当前账户
- if (!device.accountId || device.accountId !== account.id) {
- return next(errors.createError(403, "该设备未绑定到您的账户"));
- }
+ // 验证设备是否绑定到当前账户
+ if (!device.accountId || device.accountId !== account.id) {
+ return next(errors.createError(403, "该设备未绑定到您的账户"));
+ }
- // 查找自动授权配置
- const autoAuth = await prisma.autoAuth.findUnique({
- where: { id: configId },
- });
+ // 查找自动授权配置
+ const autoAuth = await prisma.autoAuth.findUnique({
+ where: {id: configId},
+ });
- if (!autoAuth) {
- return next(errors.createError(404, "自动授权配置不存在"));
- }
+ if (!autoAuth) {
+ return next(errors.createError(404, "自动授权配置不存在"));
+ }
- // 确保配置属于当前设备
- if (autoAuth.deviceId !== device.id) {
- return next(errors.createError(403, "无权操作此配置"));
- }
+ // 确保配置属于当前设备
+ if (autoAuth.deviceId !== device.id) {
+ return next(errors.createError(403, "无权操作此配置"));
+ }
- // 验证 deviceType
- const validDeviceTypes = ['teacher', 'student', 'classroom', 'parent'];
- if (deviceType && !validDeviceTypes.includes(deviceType)) {
- return next(errors.createError(400, `设备类型必须是以下之一: ${validDeviceTypes.join(', ')}`));
- }
+ // 验证 deviceType
+ const validDeviceTypes = ['teacher', 'student', 'classroom', 'parent'];
+ if (deviceType && !validDeviceTypes.includes(deviceType)) {
+ return next(errors.createError(400, `设备类型必须是以下之一: ${validDeviceTypes.join(', ')}`));
+ }
- // 准备更新数据
- const updateData = {};
+ // 准备更新数据
+ const updateData = {};
- if (password !== undefined) {
- // 规范化密码:空字符串视为 null
- const plainPassword = (password !== '') ? password : null;
+ if (password !== undefined) {
+ // 规范化密码:空字符串视为 null
+ const plainPassword = (password !== '') ? password : null;
- // 查询该设备的所有配置,本地检查新密码是否与其他配置冲突
- const allAuths = await prisma.autoAuth.findMany({
- where: { deviceId: device.id },
- });
+ // 查询该设备的所有配置,本地检查新密码是否与其他配置冲突
+ const allAuths = await prisma.autoAuth.findMany({
+ where: {deviceId: device.id},
+ });
- const conflictAuth = allAuths.find(auth =>
- auth.id !== configId && auth.password === plainPassword
- );
+ const conflictAuth = allAuths.find(auth =>
+ auth.id !== configId && auth.password === plainPassword
+ );
- if (conflictAuth) {
- return next(errors.createError(400, "该密码已被其他配置使用"));
- }
+ if (conflictAuth) {
+ return next(errors.createError(400, "该密码已被其他配置使用"));
+ }
- updateData.password = plainPassword;
- }
+ updateData.password = plainPassword;
+ }
- if (deviceType !== undefined) {
- updateData.deviceType = deviceType || null;
- }
+ if (deviceType !== undefined) {
+ updateData.deviceType = deviceType || null;
+ }
- if (isReadOnly !== undefined) {
- updateData.isReadOnly = isReadOnly;
- }
+ if (isReadOnly !== undefined) {
+ updateData.isReadOnly = isReadOnly;
+ }
- // 更新配置
- const updatedAuth = await prisma.autoAuth.update({
- where: { id: configId },
- data: updateData,
- });
+ // 更新配置
+ const updatedAuth = await prisma.autoAuth.update({
+ where: {id: configId},
+ data: updateData,
+ });
- return res.json({
- success: true,
- config: {
- id: updatedAuth.id,
- password: updatedAuth.password, // 返回明文密码
- deviceType: updatedAuth.deviceType,
- isReadOnly: updatedAuth.isReadOnly,
- updatedAt: updatedAuth.updatedAt,
- },
- });
- })
+ return res.json({
+ success: true,
+ config: {
+ id: updatedAuth.id,
+ password: updatedAuth.password, // 返回明文密码
+ deviceType: updatedAuth.deviceType,
+ isReadOnly: updatedAuth.isReadOnly,
+ updatedAt: updatedAuth.updatedAt,
+ },
+ });
+ })
);
/**
@@ -229,47 +231,47 @@ router.put(
* 删除自动授权配置 (需要 JWT 认证,且设备必须绑定到该账户)
*/
router.delete(
- "/devices/:uuid/auth-configs/:configId",
- jwtAuth,
- errors.catchAsync(async (req, res, next) => {
- const { uuid, configId } = req.params;
- const account = res.locals.account;
+ "/devices/:uuid/auth-configs/:configId",
+ jwtAuth,
+ errors.catchAsync(async (req, res, next) => {
+ const {uuid, configId} = req.params;
+ const account = res.locals.account;
- // 查找设备并验证是否属于当前账户
- const device = await prisma.device.findUnique({
- where: { uuid },
- });
+ // 查找设备并验证是否属于当前账户
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ });
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
+ }
- // 验证设备是否绑定到当前账户
- if (!device.accountId || device.accountId !== account.id) {
- return next(errors.createError(403, "该设备未绑定到您的账户"));
- }
+ // 验证设备是否绑定到当前账户
+ if (!device.accountId || device.accountId !== account.id) {
+ return next(errors.createError(403, "该设备未绑定到您的账户"));
+ }
- // 查找自动授权配置
- const autoAuth = await prisma.autoAuth.findUnique({
- where: { id: configId },
- });
+ // 查找自动授权配置
+ const autoAuth = await prisma.autoAuth.findUnique({
+ where: {id: configId},
+ });
- if (!autoAuth) {
- return next(errors.createError(404, "自动授权配置不存在"));
- }
+ if (!autoAuth) {
+ return next(errors.createError(404, "自动授权配置不存在"));
+ }
- // 确保配置属于当前设备
- if (autoAuth.deviceId !== device.id) {
- return next(errors.createError(403, "无权操作此配置"));
- }
+ // 确保配置属于当前设备
+ if (autoAuth.deviceId !== device.id) {
+ return next(errors.createError(403, "无权操作此配置"));
+ }
- // 删除配置
- await prisma.autoAuth.delete({
- where: { id: configId },
- });
+ // 删除配置
+ await prisma.autoAuth.delete({
+ where: {id: configId},
+ });
- return res.status(204).end();
- })
+ return res.status(204).end();
+ })
);
/**
@@ -278,66 +280,66 @@ router.delete(
* Body: { namespace: string }
*/
router.put(
- "/devices/:uuid/namespace",
- jwtAuth,
- errors.catchAsync(async (req, res, next) => {
- const { uuid } = req.params;
- const account = res.locals.account;
- const { namespace } = req.body;
+ "/devices/:uuid/namespace",
+ jwtAuth,
+ errors.catchAsync(async (req, res, next) => {
+ const {uuid} = req.params;
+ const account = res.locals.account;
+ const {namespace} = req.body;
- if (!namespace) {
- return next(errors.createError(400, "需要提供 namespace"));
- }
+ if (!namespace) {
+ return next(errors.createError(400, "需要提供 namespace"));
+ }
- // 规范化 namespace:去除首尾空格
- const trimmedNamespace = namespace.trim();
+ // 规范化 namespace:去除首尾空格
+ const trimmedNamespace = namespace.trim();
- if (!trimmedNamespace) {
- return next(errors.createError(400, "namespace 不能为空"));
- }
+ if (!trimmedNamespace) {
+ return next(errors.createError(400, "namespace 不能为空"));
+ }
- // 查找设备并验证是否属于当前账户
- const device = await prisma.device.findUnique({
- where: { uuid },
- });
+ // 查找设备并验证是否属于当前账户
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ });
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
+ }
- // 验证设备是否绑定到当前账户
- if (!device.accountId || device.accountId !== account.id) {
- return next(errors.createError(403, "该设备未绑定到您的账户"));
- }
+ // 验证设备是否绑定到当前账户
+ if (!device.accountId || device.accountId !== account.id) {
+ return next(errors.createError(403, "该设备未绑定到您的账户"));
+ }
- // 检查新的 namespace 是否已被其他设备使用
- if (device.namespace !== trimmedNamespace) {
- const existingDevice = await prisma.device.findUnique({
- where: { namespace: trimmedNamespace },
- });
+ // 检查新的 namespace 是否已被其他设备使用
+ if (device.namespace !== trimmedNamespace) {
+ const existingDevice = await prisma.device.findUnique({
+ where: {namespace: trimmedNamespace},
+ });
- if (existingDevice) {
- return next(errors.createError(409, "该 namespace 已被其他设备使用"));
- }
- }
+ if (existingDevice) {
+ return next(errors.createError(409, "该 namespace 已被其他设备使用"));
+ }
+ }
- // 更新设备的 namespace
- const updatedDevice = await prisma.device.update({
- where: { id: device.id },
- data: { namespace: trimmedNamespace },
- });
+ // 更新设备的 namespace
+ const updatedDevice = await prisma.device.update({
+ where: {id: device.id},
+ data: {namespace: trimmedNamespace},
+ });
- return res.json({
- success: true,
- device: {
- id: updatedDevice.id,
- uuid: updatedDevice.uuid,
- name: updatedDevice.name,
- namespace: updatedDevice.namespace,
- updatedAt: updatedDevice.updatedAt,
- },
- });
- })
+ return res.json({
+ success: true,
+ device: {
+ id: updatedDevice.id,
+ uuid: updatedDevice.uuid,
+ name: updatedDevice.name,
+ namespace: updatedDevice.namespace,
+ updatedAt: updatedDevice.updatedAt,
+ },
+ });
+ })
);
export default router;
diff --git a/routes/device-auth.js b/routes/device-auth.js
index 66f90a6..04aa223 100644
--- a/routes/device-auth.js
+++ b/routes/device-auth.js
@@ -1,13 +1,12 @@
-import { Router } from "express";
+import {Router} from "express";
import deviceCodeStore from "../utils/deviceCodeStore.js";
import errors from "../utils/errors.js";
-import { PrismaClient } from "@prisma/client";
+import {PrismaClient} from "@prisma/client";
const router = Router();
const prisma = new PrismaClient();
-
/**
* POST /device/code
* 生成设备授权码
@@ -21,16 +20,16 @@ const prisma = new PrismaClient();
* }
*/
router.post(
- "/device/code",
- errors.catchAsync(async (req, res) => {
- const deviceCode = deviceCodeStore.create();
+ "/device/code",
+ errors.catchAsync(async (req, res) => {
+ const deviceCode = deviceCodeStore.create();
- return res.json({
- device_code: deviceCode,
- expires_in: 900, // 15分钟
- message: "请在前端输入此代码进行授权",
- });
- })
+ return res.json({
+ device_code: deviceCode,
+ expires_in: 900, // 15分钟
+ message: "请在前端输入此代码进行授权",
+ });
+ })
);
/**
@@ -53,39 +52,39 @@ router.post(
* }
*/
router.post(
- "/device/bind",
- errors.catchAsync(async (req, res, next) => {
- const { device_code, token } = req.body;
+ "/device/bind",
+ errors.catchAsync(async (req, res, next) => {
+ const {device_code, token} = req.body;
- if (!device_code || !token) {
- return next(
- errors.createError(400, "请提供 device_code 和 token")
- );
- }
+ if (!device_code || !token) {
+ return next(
+ errors.createError(400, "请提供 device_code 和 token")
+ );
+ }
- // 验证token是否有效(检查数据库)
- const appInstall = await prisma.appInstall.findUnique({
- where: { token },
- });
+ // 验证token是否有效(检查数据库)
+ const appInstall = await prisma.appInstall.findUnique({
+ where: {token},
+ });
- if (!appInstall) {
- return next(errors.createError(400, "无效的令牌"));
- }
+ if (!appInstall) {
+ return next(errors.createError(400, "无效的令牌"));
+ }
- // 绑定令牌到设备代码
- const success = deviceCodeStore.bindToken(device_code, token);
+ // 绑定令牌到设备代码
+ const success = deviceCodeStore.bindToken(device_code, token);
- if (!success) {
- return next(
- errors.createError(400, "设备代码不存在或已过期")
- );
- }
+ if (!success) {
+ return next(
+ errors.createError(400, "设备代码不存在或已过期")
+ );
+ }
- return res.json({
- success: true,
- message: "令牌已成功绑定到设备代码",
- });
- })
+ return res.json({
+ success: true,
+ message: "令牌已成功绑定到设备代码",
+ });
+ })
);
/**
@@ -117,43 +116,43 @@ router.post(
* }
*/
router.get(
- "/device/token",
- errors.catchAsync(async (req, res, next) => {
- const { device_code } = req.query;
+ "/device/token",
+ errors.catchAsync(async (req, res, next) => {
+ const {device_code} = req.query;
- if (!device_code) {
- return next(errors.createError(400, "请提供 device_code"));
- }
+ if (!device_code) {
+ return next(errors.createError(400, "请提供 device_code"));
+ }
- // 尝试获取并移除令牌
- const token = deviceCodeStore.getAndRemove(device_code);
+ // 尝试获取并移除令牌
+ const token = deviceCodeStore.getAndRemove(device_code);
- if (token) {
- // 令牌已绑定,返回并删除
- return res.json({
- status: "success",
- token,
- });
- }
+ if (token) {
+ // 令牌已绑定,返回并删除
+ return res.json({
+ status: "success",
+ token,
+ });
+ }
- // 检查设备代码是否存在
- const status = deviceCodeStore.getStatus(device_code);
+ // 检查设备代码是否存在
+ const status = deviceCodeStore.getStatus(device_code);
- if (!status) {
- // 设备代码不存在或已过期
- return res.json({
- status: "expired",
- message: "设备代码不存在或已过期",
- });
- }
+ if (!status) {
+ // 设备代码不存在或已过期
+ return res.json({
+ status: "expired",
+ message: "设备代码不存在或已过期",
+ });
+ }
- // 设备代码存在但令牌未绑定
- return res.json({
- status: "pending",
- message: "等待用户授权",
- expires_in: Math.floor((status.expiresAt - Date.now()) / 1000),
- });
- })
+ // 设备代码存在但令牌未绑定
+ return res.json({
+ status: "pending",
+ message: "等待用户授权",
+ expires_in: Math.floor((status.expiresAt - Date.now()) / 1000),
+ });
+ })
);
/**
@@ -172,32 +171,32 @@ router.get(
* }
*/
router.get(
- "/device/status",
- errors.catchAsync(async (req, res, next) => {
- const { device_code } = req.query;
+ "/device/status",
+ errors.catchAsync(async (req, res, next) => {
+ const {device_code} = req.query;
- if (!device_code) {
- return next(errors.createError(400, "请提供 device_code"));
- }
+ if (!device_code) {
+ return next(errors.createError(400, "请提供 device_code"));
+ }
- const status = deviceCodeStore.getStatus(device_code);
+ const status = deviceCodeStore.getStatus(device_code);
- if (!status) {
- return res.json({
- device_code,
- exists: false,
- message: "设备代码不存在或已过期",
- });
- }
+ if (!status) {
+ return res.json({
+ device_code,
+ exists: false,
+ message: "设备代码不存在或已过期",
+ });
+ }
- return res.json({
- device_code,
- exists: true,
- has_token: status.hasToken,
- expires_in: Math.floor((status.expiresAt - Date.now()) / 1000),
- created_at: status.createdAt,
- });
- })
+ return res.json({
+ device_code,
+ exists: true,
+ has_token: status.hasToken,
+ expires_in: Math.floor((status.expiresAt - Date.now()) / 1000),
+ created_at: status.createdAt,
+ });
+ })
);
export default router;
diff --git a/routes/device.js b/routes/device.js
index c6d09d6..db0677b 100644
--- a/routes/device.js
+++ b/routes/device.js
@@ -1,12 +1,11 @@
-import { Router } from "express";
-const router = Router();
-import { uuidAuth, extractDeviceInfo } from "../middleware/uuidAuth.js";
-import { PrismaClient } from "@prisma/client";
-import crypto from "crypto";
+import {Router} from "express";
+import {extractDeviceInfo} from "../middleware/uuidAuth.js";
+import {PrismaClient} from "@prisma/client";
import errors from "../utils/errors.js";
-import { hashPassword, verifyDevicePassword } from "../utils/crypto.js";
-import { getOnlineDevices } from "../utils/socket.js";
-import { registeredDevicesTotal } from "../utils/metrics.js";
+import {getOnlineDevices} from "../utils/socket.js";
+import {registeredDevicesTotal} from "../utils/metrics.js";
+
+const router = Router();
const prisma = new PrismaClient();
@@ -15,20 +14,20 @@ const prisma = new PrismaClient();
* @param {number} deviceId - 设备ID
*/
async function createDefaultAutoAuth(deviceId) {
- try {
- // 创建默认的自动授权配置:不需要密码、类型是classroom(一体机)
- await prisma.autoAuth.create({
- data: {
- deviceId: deviceId,
- password: null, // 无密码
- deviceType: "classroom", // 一体机类型
- isReadOnly: false, // 非只读
- },
- });
- } catch (error) {
- console.error('创建默认自动登录配置失败:', error);
- // 这里不抛出错误,避免影响设备创建流程
- }
+ try {
+ // 创建默认的自动授权配置:不需要密码、类型是classroom(一体机)
+ await prisma.autoAuth.create({
+ data: {
+ deviceId: deviceId,
+ password: null, // 无密码
+ deviceType: "classroom", // 一体机类型
+ isReadOnly: false, // 非只读
+ },
+ });
+ } catch (error) {
+ console.error('创建默认自动登录配置失败:', error);
+ // 这里不抛出错误,避免影响设备创建流程
+ }
}
/**
@@ -36,70 +35,70 @@ async function createDefaultAutoAuth(deviceId) {
* 注册新设备
*/
router.post(
- "/",
- errors.catchAsync(async (req, res, next) => {
- const { uuid, deviceName, namespace } = req.body;
+ "/",
+ errors.catchAsync(async (req, res, next) => {
+ const {uuid, deviceName, namespace} = req.body;
- if (!uuid) {
- return next(errors.createError(400, "设备UUID是必需的"));
- }
+ if (!uuid) {
+ return next(errors.createError(400, "设备UUID是必需的"));
+ }
- if (!deviceName) {
- return next(errors.createError(400, "设备名称是必需的"));
- }
+ if (!deviceName) {
+ return next(errors.createError(400, "设备名称是必需的"));
+ }
- try {
- // 检查UUID是否已存在
- const existingDevice = await prisma.device.findUnique({
- where: { uuid },
- });
+ try {
+ // 检查UUID是否已存在
+ const existingDevice = await prisma.device.findUnique({
+ where: {uuid},
+ });
- if (existingDevice) {
- return next(errors.createError(409, "设备UUID已存在"));
- }
+ if (existingDevice) {
+ return next(errors.createError(409, "设备UUID已存在"));
+ }
- // 处理 namespace:如果没有提供,则使用 uuid
- const deviceNamespace = namespace && namespace.trim() ? namespace.trim() : uuid;
+ // 处理 namespace:如果没有提供,则使用 uuid
+ const deviceNamespace = namespace && namespace.trim() ? namespace.trim() : uuid;
- // 检查 namespace 是否已被使用
- const existingNamespace = await prisma.device.findUnique({
- where: { namespace: deviceNamespace },
- });
+ // 检查 namespace 是否已被使用
+ const existingNamespace = await prisma.device.findUnique({
+ where: {namespace: deviceNamespace},
+ });
- if (existingNamespace) {
- return next(errors.createError(409, "该 namespace 已被使用"));
- }
+ if (existingNamespace) {
+ return next(errors.createError(409, "该 namespace 已被使用"));
+ }
- // 创建设备
- const device = await prisma.device.create({
- data: {
- uuid,
- name: deviceName,
- namespace: deviceNamespace,
- },
- });
+ // 创建设备
+ const device = await prisma.device.create({
+ data: {
+ uuid,
+ name: deviceName,
+ namespace: deviceNamespace,
+ },
+ });
- // 为新设备创建默认的自动登录配置
- await createDefaultAutoAuth(device.id);
+ // 为新设备创建默认的自动登录配置
+ await createDefaultAutoAuth(device.id);
- // 更新注册设备总数指标
- const totalDevices = await prisma.device.count();
- registeredDevicesTotal.set(totalDevices);
+ // 更新注册设备总数指标
+ const totalDevices = await prisma.device.count();
+ registeredDevicesTotal.set(totalDevices);
- return res.status(201).json({
- success: true,
- device: {
- id: device.id,
- uuid: device.uuid,
- name: device.name,
- namespace: device.namespace,
- createdAt: device.createdAt,
- },
- });
- } catch (error) {
- throw error;
- }
- })
+ return res.status(201).json({
+ success: true,
+ device: {
+ id: device.id,
+ uuid: device.uuid,
+ name: device.name,
+ namespace: device.namespace,
+ createdAt: device.createdAt,
+ },
+ });
+ } catch (error) {
+ throw error;
+ }
+ })
);
/**
@@ -107,111 +106,111 @@ router.post(
* 获取设备信息 (公开接口,无需认证)
*/
router.get(
- "/:uuid",
- errors.catchAsync(async (req, res, next) => {
- const { uuid } = req.params;
+ "/:uuid",
+ errors.catchAsync(async (req, res, next) => {
+ const {uuid} = req.params;
- // 查找设备,包含绑定的账户信息
- const device = await prisma.device.findUnique({
- where: { uuid },
- include: {
- account: {
- select: {
- id: true,
- name: true,
- email: true,
- avatarUrl: true,
- },
- },
- },
- });
+ // 查找设备,包含绑定的账户信息
+ const device = await prisma.device.findUnique({
+ where: {uuid},
+ include: {
+ account: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ avatarUrl: true,
+ },
+ },
+ },
+ });
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
+ }
- return res.json({
- id: device.id,
- uuid: device.uuid,
- name: device.name,
- hasPassword: !!device.password,
- passwordHint: device.passwordHint,
- createdAt: device.createdAt,
- account: device.account ? {
- id: device.account.id,
- name: device.account.name,
- email: device.account.email,
- avatarUrl: device.account.avatarUrl,
- } : null,
- isBoundToAccount: !!device.account,
- namespace: device.namespace,
- });
- })
-);/**
+ return res.json({
+ id: device.id,
+ uuid: device.uuid,
+ name: device.name,
+ hasPassword: !!device.password,
+ passwordHint: device.passwordHint,
+ createdAt: device.createdAt,
+ account: device.account ? {
+ id: device.account.id,
+ name: device.account.name,
+ email: device.account.email,
+ avatarUrl: device.account.avatarUrl,
+ } : null,
+ isBoundToAccount: !!device.account,
+ namespace: device.namespace,
+ });
+ })
+);
+/**
* PUT /devices/:uuid/name
* 设置设备名称 (需要UUID认证)
*/
router.put(
- "/:uuid/name",
- extractDeviceInfo,
- errors.catchAsync(async (req, res, next) => {
- const { name } = req.body;
- const device = res.locals.device;
+ "/:uuid/name",
+ extractDeviceInfo,
+ errors.catchAsync(async (req, res, next) => {
+ const {name} = req.body;
+ const device = res.locals.device;
- if (!name) {
- return next(errors.createError(400, "设备名称是必需的"));
- }
+ if (!name) {
+ return next(errors.createError(400, "设备名称是必需的"));
+ }
- const updatedDevice = await prisma.device.update({
- where: { id: device.id },
- data: { name },
- });
+ const updatedDevice = await prisma.device.update({
+ where: {id: device.id},
+ data: {name},
+ });
- return res.json({
- success: true,
- device: {
- id: updatedDevice.id,
- uuid: updatedDevice.uuid,
- name: updatedDevice.name,
- hasPassword: !!updatedDevice.password,
- passwordHint: updatedDevice.passwordHint,
- },
- });
- })
+ return res.json({
+ success: true,
+ device: {
+ id: updatedDevice.id,
+ uuid: updatedDevice.uuid,
+ name: updatedDevice.name,
+ hasPassword: !!updatedDevice.password,
+ passwordHint: updatedDevice.passwordHint,
+ },
+ });
+ })
);
-
/**
* GET /devices/online
* 查询在线设备(WebSocket 已连接)
* 返回:[{ uuid, connections, name? }]
*/
router.get(
- "/online",
- errors.catchAsync(async (req, res) => {
- const list = getOnlineDevices();
+ "/online",
+ errors.catchAsync(async (req, res) => {
+ const list = getOnlineDevices();
- if (list.length === 0) {
- return res.json({ success: true, devices: [] });
- }
+ if (list.length === 0) {
+ return res.json({success: true, devices: []});
+ }
- // 补充设备名称
- const uuids = list.map((x) => x.uuid);
- const rows = await prisma.device.findMany({
- where: { uuid: { in: uuids } },
- select: { uuid: true, name: true },
- });
- const nameMap = new Map(rows.map((r) => [r.uuid, r.name]));
+ // 补充设备名称
+ const uuids = list.map((x) => x.uuid);
+ const rows = await prisma.device.findMany({
+ where: {uuid: {in: uuids}},
+ select: {uuid: true, name: true},
+ });
+ const nameMap = new Map(rows.map((r) => [r.uuid, r.name]));
- const devices = list.map((x) => ({
- uuid: x.uuid,
- connections: x.connections,
- name: nameMap.get(x.uuid) || null,
- }));
+ const devices = list.map((x) => ({
+ uuid: x.uuid,
+ connections: x.connections,
+ name: nameMap.get(x.uuid) || null,
+ }));
- res.json({ success: true, devices });
- })
+ res.json({success: true, devices});
+ })
);
export default router;
diff --git a/routes/index.js b/routes/index.js
index 0061319..8a964de 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -1,9 +1,10 @@
-import { Router } from "express";
+import {Router} from "express";
+
var router = Router();
/* GET home page. */
router.get("/", function (req, res, next) {
- res.render("index");
+ res.render("index");
});
export default router;
diff --git a/routes/kv-token.js b/routes/kv-token.js
index 2cb0ea7..8090577 100644
--- a/routes/kv-token.js
+++ b/routes/kv-token.js
@@ -1,17 +1,18 @@
-import { Router } from "express";
-const router = Router();
+import {Router} from "express";
import kvStore from "../utils/kvStore.js";
-import { broadcastKeyChanged } from "../utils/socket.js";
-import { kvTokenAuth } from "../middleware/kvTokenAuth.js";
+import {broadcastKeyChanged} from "../utils/socket.js";
+import {kvTokenAuth} from "../middleware/kvTokenAuth.js";
import {
- tokenReadLimiter,
- tokenWriteLimiter,
- tokenDeleteLimiter,
- tokenBatchLimiter,
- prepareTokenForRateLimit
+ prepareTokenForRateLimit,
+ tokenBatchLimiter,
+ tokenDeleteLimiter,
+ tokenReadLimiter,
+ tokenWriteLimiter
} from "../middleware/rateLimiter.js";
import errors from "../utils/errors.js";
-import { PrismaClient } from "@prisma/client";
+import {PrismaClient} from "@prisma/client";
+
+const router = Router();
const prisma = new PrismaClient();
@@ -26,52 +27,52 @@ router.use(prepareTokenForRateLimit);
* 获取当前token所属设备的信息,如果关联了账号也返回账号信息
*/
router.get(
- "/_info",
- tokenReadLimiter,
- errors.catchAsync(async (req, res, next) => {
- const deviceId = res.locals.deviceId;
+ "/_info",
+ tokenReadLimiter,
+ errors.catchAsync(async (req, res, next) => {
+ const deviceId = res.locals.deviceId;
- // 获取设备信息,包含关联的账号
- const device = await prisma.device.findUnique({
- where: { id: deviceId },
- include: {
- account: true,
- },
- });
+ // 获取设备信息,包含关联的账号
+ const device = await prisma.device.findUnique({
+ where: {id: deviceId},
+ include: {
+ account: true,
+ },
+ });
- if (!device) {
- return next(errors.createError(404, "设备不存在"));
- }
+ if (!device) {
+ return next(errors.createError(404, "设备不存在"));
+ }
- // 构建响应对象:当设备没有关联账号时返回 uuid;若已关联账号则不返回 uuid
- const response = {
- device: {
- id: device.id,
- name: device.name,
- createdAt: device.createdAt,
- updatedAt: device.updatedAt,
- },
- };
+ // 构建响应对象:当设备没有关联账号时返回 uuid;若已关联账号则不返回 uuid
+ const response = {
+ device: {
+ id: device.id,
+ name: device.name,
+ createdAt: device.createdAt,
+ updatedAt: device.updatedAt,
+ },
+ };
- // 仅当设备未绑定账号时,包含 uuid 字段
- if (!device.account) {
- response.device.uuid = device.uuid;
- }
+ // 仅当设备未绑定账号时,包含 uuid 字段
+ if (!device.account) {
+ response.device.uuid = device.uuid;
+ }
- // 标识是否已绑定账号
- response.hasAccount = !!device.account;
+ // 标识是否已绑定账号
+ response.hasAccount = !!device.account;
- // 如果关联了账号,添加账号信息
- if (device.account) {
- response.account = {
- id: device.account.id,
- name: device.account.name,
- avatarUrl: device.account.avatarUrl,
- };
- }
+ // 如果关联了账号,添加账号信息
+ if (device.account) {
+ response.account = {
+ id: device.account.id,
+ name: device.account.name,
+ avatarUrl: device.account.avatarUrl,
+ };
+ }
- return res.json(response);
- })
+ return res.json(response);
+ })
);
/**
@@ -79,48 +80,48 @@ router.get(
* 获取当前 KV Token 的详细信息(类型、备注等)
*/
router.get(
- "/_token",
- tokenReadLimiter,
- errors.catchAsync(async (req, res, next) => {
- const token = res.locals.token;
- const deviceId = res.locals.deviceId;
+ "/_token",
+ tokenReadLimiter,
+ errors.catchAsync(async (req, res, next) => {
+ const token = res.locals.token;
+ const deviceId = res.locals.deviceId;
- // 查找当前 token 对应的应用安装记录
- const appInstall = await prisma.appInstall.findUnique({
- where: { token },
- include: {
- device: {
- select: {
- id: true,
- uuid: true,
- name: true,
- namespace: true,
- },
- },
- },
- });
+ // 查找当前 token 对应的应用安装记录
+ const appInstall = await prisma.appInstall.findUnique({
+ where: {token},
+ include: {
+ device: {
+ select: {
+ id: true,
+ uuid: true,
+ name: true,
+ namespace: true,
+ },
+ },
+ },
+ });
- if (!appInstall) {
- return next(errors.createError(404, "Token 信息不存在"));
- }
+ if (!appInstall) {
+ return next(errors.createError(404, "Token 信息不存在"));
+ }
- return res.json({
- success: true,
- token: appInstall.token,
- appId: appInstall.appId,
- deviceType: appInstall.deviceType,
- isReadOnly: appInstall.isReadOnly,
- note: appInstall.note,
- installedAt: appInstall.installedAt,
- updatedAt: appInstall.updatedAt,
- device: {
- id: appInstall.device.id,
- uuid: appInstall.device.uuid,
- name: appInstall.device.name,
- namespace: appInstall.device.namespace,
- },
- });
- })
+ return res.json({
+ success: true,
+ token: appInstall.token,
+ appId: appInstall.appId,
+ deviceType: appInstall.deviceType,
+ isReadOnly: appInstall.isReadOnly,
+ note: appInstall.note,
+ installedAt: appInstall.installedAt,
+ updatedAt: appInstall.updatedAt,
+ device: {
+ id: appInstall.device.id,
+ uuid: appInstall.device.uuid,
+ name: appInstall.device.name,
+ namespace: appInstall.device.namespace,
+ },
+ });
+ })
);
/**
@@ -128,50 +129,50 @@ router.get(
* 获取当前token对应设备的键名列表(分页,不包括内容)
*/
router.get(
- "/_keys",
- tokenReadLimiter,
- errors.catchAsync(async (req, res) => {
- const deviceId = res.locals.deviceId;
- const { sortBy, sortDir, limit, skip } = req.query;
+ "/_keys",
+ tokenReadLimiter,
+ errors.catchAsync(async (req, res) => {
+ const deviceId = res.locals.deviceId;
+ const {sortBy, sortDir, limit, skip} = req.query;
- // 构建选项
- const options = {
- sortBy: sortBy || "key",
- sortDir: sortDir || "asc",
- limit: limit ? parseInt(limit) : 100,
- skip: skip ? parseInt(skip) : 0,
- };
+ // 构建选项
+ const options = {
+ sortBy: sortBy || "key",
+ sortDir: sortDir || "asc",
+ limit: limit ? parseInt(limit) : 100,
+ skip: skip ? parseInt(skip) : 0,
+ };
- const keys = await kvStore.listKeysOnly(deviceId, options);
- const totalRows = keys.length;
+ const keys = await kvStore.listKeysOnly(deviceId, options);
+ const totalRows = keys.length;
- // 构建响应对象
- const response = {
- keys: keys,
- total_rows: totalRows,
- current_page: {
- limit: options.limit,
- skip: options.skip,
- count: keys.length,
- },
- };
+ // 构建响应对象
+ const response = {
+ keys: keys,
+ total_rows: totalRows,
+ current_page: {
+ limit: options.limit,
+ skip: options.skip,
+ count: keys.length,
+ },
+ };
- // 如果还有更多数据,添加load_more字段
- const nextSkip = options.skip + options.limit;
- if (nextSkip < totalRows) {
- const baseUrl = `${req.baseUrl}/_keys`;
- const queryParams = new URLSearchParams({
- sortBy: options.sortBy,
- sortDir: options.sortDir,
- limit: options.limit,
- skip: nextSkip,
- }).toString();
+ // 如果还有更多数据,添加load_more字段
+ const nextSkip = options.skip + options.limit;
+ if (nextSkip < totalRows) {
+ const baseUrl = `${req.baseUrl}/_keys`;
+ const queryParams = new URLSearchParams({
+ sortBy: options.sortBy,
+ sortDir: options.sortDir,
+ limit: options.limit,
+ skip: nextSkip,
+ }).toString();
- response.load_more = `${baseUrl}?${queryParams}`;
- }
+ response.load_more = `${baseUrl}?${queryParams}`;
+ }
- return res.json(response);
- })
+ return res.json(response);
+ })
);
/**
@@ -179,45 +180,45 @@ router.get(
* 获取当前token对应设备的所有键名及元数据列表
*/
router.get(
- "/",
- tokenReadLimiter,
- errors.catchAsync(async (req, res) => {
- const deviceId = res.locals.deviceId;
- const { sortBy, sortDir, limit, skip } = req.query;
+ "/",
+ tokenReadLimiter,
+ errors.catchAsync(async (req, res) => {
+ const deviceId = res.locals.deviceId;
+ const {sortBy, sortDir, limit, skip} = req.query;
- // 构建选项
- const options = {
- sortBy: sortBy || "key",
- sortDir: sortDir || "asc",
- limit: limit ? parseInt(limit) : 100,
- skip: skip ? parseInt(skip) : 0,
- };
+ // 构建选项
+ const options = {
+ sortBy: sortBy || "key",
+ sortDir: sortDir || "asc",
+ limit: limit ? parseInt(limit) : 100,
+ skip: skip ? parseInt(skip) : 0,
+ };
- const keys = await kvStore.list(deviceId, options);
- const totalRows = await kvStore.count(deviceId);
+ const keys = await kvStore.list(deviceId, options);
+ const totalRows = await kvStore.count(deviceId);
- // 构建响应对象
- const response = {
- items: keys,
- total_rows: totalRows,
- };
+ // 构建响应对象
+ const response = {
+ items: keys,
+ total_rows: totalRows,
+ };
- // 如果还有更多数据,添加load_more字段
- const nextSkip = options.skip + options.limit;
- if (nextSkip < totalRows) {
- const baseUrl = `${req.baseUrl}`;
- const queryParams = new URLSearchParams({
- sortBy: options.sortBy,
- sortDir: options.sortDir,
- limit: options.limit,
- skip: nextSkip,
- }).toString();
+ // 如果还有更多数据,添加load_more字段
+ const nextSkip = options.skip + options.limit;
+ if (nextSkip < totalRows) {
+ const baseUrl = `${req.baseUrl}`;
+ const queryParams = new URLSearchParams({
+ sortBy: options.sortBy,
+ sortDir: options.sortDir,
+ limit: options.limit,
+ skip: nextSkip,
+ }).toString();
- response.load_more = `${baseUrl}?${queryParams}`;
- }
+ response.load_more = `${baseUrl}?${queryParams}`;
+ }
- return res.json(response);
- })
+ return res.json(response);
+ })
);
/**
@@ -225,22 +226,22 @@ router.get(
* 通过键名获取键值
*/
router.get(
- "/:key",
- tokenReadLimiter,
- errors.catchAsync(async (req, res, next) => {
- const deviceId = res.locals.deviceId;
- const { key } = req.params;
+ "/:key",
+ tokenReadLimiter,
+ errors.catchAsync(async (req, res, next) => {
+ const deviceId = res.locals.deviceId;
+ const {key} = req.params;
- const value = await kvStore.get(deviceId, key);
+ const value = await kvStore.get(deviceId, key);
- if (value === null) {
- return next(
- errors.createError(404, `未找到键名为 '${key}' 的记录`)
- );
- }
+ if (value === null) {
+ return next(
+ errors.createError(404, `未找到键名为 '${key}' 的记录`)
+ );
+ }
- return res.json(value);
- })
+ return res.json(value);
+ })
);
/**
@@ -248,20 +249,20 @@ router.get(
* 获取键的元数据
*/
router.get(
- "/:key/metadata",
- tokenReadLimiter,
- errors.catchAsync(async (req, res, next) => {
- const deviceId = res.locals.deviceId;
- const { key } = req.params;
+ "/:key/metadata",
+ tokenReadLimiter,
+ errors.catchAsync(async (req, res, next) => {
+ const deviceId = res.locals.deviceId;
+ const {key} = req.params;
- const metadata = await kvStore.getMetadata(deviceId, key);
- if (!metadata) {
- return next(
- errors.createError(404, `未找到键名为 '${key}' 的记录`)
- );
- }
- return res.json(metadata);
- })
+ const metadata = await kvStore.getMetadata(deviceId, key);
+ if (!metadata) {
+ return next(
+ errors.createError(404, `未找到键名为 '${key}' 的记录`)
+ );
+ }
+ return res.json(metadata);
+ })
);
/**
@@ -269,73 +270,73 @@ router.get(
* 批量导入键值对
*/
router.post(
- "/_batchimport",
- tokenBatchLimiter,
- errors.catchAsync(async (req, res, next) => {
- // 检查token是否为只读
- if (res.locals.appInstall?.isReadOnly) {
- return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
- }
-
- const deviceId = res.locals.deviceId;
- const data = req.body;
-
- if (!data || Object.keys(data).length === 0) {
- return next(
- errors.createError(
- 400,
- '请提供有效的JSON数据,格式为 {"key":{}, "key2":{}}'
- )
- );
- }
-
- // 获取客户端IP
- const creatorIp =
- req.headers["x-forwarded-for"] ||
- req.connection.remoteAddress ||
- req.socket.remoteAddress ||
- req.connection.socket?.remoteAddress ||
- "";
-
- const results = [];
- const errorList = [];
-
- // 批量处理所有键值对
- for (const [key, value] of Object.entries(data)) {
- try {
- const result = await kvStore.upsert(deviceId, key, value, creatorIp);
- results.push({
- key: result.key,
- created: result.createdAt.getTime() === result.updatedAt.getTime(),
- });
- // 广播每个键的变更
- const uuid = res.locals.device?.uuid;
- if (uuid) {
- broadcastKeyChanged(uuid, {
- key: result.key,
- action: "upsert",
- created: result.createdAt.getTime() === result.updatedAt.getTime(),
- updatedAt: result.updatedAt,
- batch: true,
- });
+ "/_batchimport",
+ tokenBatchLimiter,
+ errors.catchAsync(async (req, res, next) => {
+ // 检查token是否为只读
+ if (res.locals.appInstall?.isReadOnly) {
+ return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
}
- } catch (error) {
- errorList.push({
- key,
- error: error.message,
- });
- }
- }
- return res.status(200).json({
- deviceId,
- total: Object.keys(data).length,
- successful: results.length,
- failed: errorList.length,
- results,
- errors: errorList.length > 0 ? errorList : undefined,
- });
- })
+ const deviceId = res.locals.deviceId;
+ const data = req.body;
+
+ if (!data || Object.keys(data).length === 0) {
+ return next(
+ errors.createError(
+ 400,
+ '请提供有效的JSON数据,格式为 {"key":{}, "key2":{}}'
+ )
+ );
+ }
+
+ // 获取客户端IP
+ const creatorIp =
+ req.headers["x-forwarded-for"] ||
+ req.connection.remoteAddress ||
+ req.socket.remoteAddress ||
+ req.connection.socket?.remoteAddress ||
+ "";
+
+ const results = [];
+ const errorList = [];
+
+ // 批量处理所有键值对
+ for (const [key, value] of Object.entries(data)) {
+ try {
+ const result = await kvStore.upsert(deviceId, key, value, creatorIp);
+ results.push({
+ key: result.key,
+ created: result.createdAt.getTime() === result.updatedAt.getTime(),
+ });
+ // 广播每个键的变更
+ const uuid = res.locals.device?.uuid;
+ if (uuid) {
+ broadcastKeyChanged(uuid, {
+ key: result.key,
+ action: "upsert",
+ created: result.createdAt.getTime() === result.updatedAt.getTime(),
+ updatedAt: result.updatedAt,
+ batch: true,
+ });
+ }
+ } catch (error) {
+ errorList.push({
+ key,
+ error: error.message,
+ });
+ }
+ }
+
+ return res.status(200).json({
+ deviceId,
+ total: Object.keys(data).length,
+ successful: results.length,
+ failed: errorList.length,
+ results,
+ errors: errorList.length > 0 ? errorList : undefined,
+ });
+ })
);
/**
@@ -343,50 +344,50 @@ router.post(
* 更新或创建键值
*/
router.post(
- "/:key",
- tokenWriteLimiter,
- errors.catchAsync(async (req, res, next) => {
- // 检查token是否为只读
- if (res.locals.appInstall?.isReadOnly) {
- return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
- }
+ "/:key",
+ tokenWriteLimiter,
+ errors.catchAsync(async (req, res, next) => {
+ // 检查token是否为只读
+ if (res.locals.appInstall?.isReadOnly) {
+ return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
+ }
- const deviceId = res.locals.deviceId;
- const { key } = req.params;
- const value = req.body;
+ const deviceId = res.locals.deviceId;
+ const {key} = req.params;
+ const value = req.body;
- if (!value || Object.keys(value).length === 0) {
- return next(errors.createError(400, "请提供有效的JSON值"));
- }
+ if (!value || Object.keys(value).length === 0) {
+ return next(errors.createError(400, "请提供有效的JSON值"));
+ }
- // 获取客户端IP
- const creatorIp =
- req.headers["x-forwarded-for"] ||
- req.connection.remoteAddress ||
- req.socket.remoteAddress ||
- req.connection.socket?.remoteAddress ||
- "";
+ // 获取客户端IP
+ const creatorIp =
+ req.headers["x-forwarded-for"] ||
+ req.connection.remoteAddress ||
+ req.socket.remoteAddress ||
+ req.connection.socket?.remoteAddress ||
+ "";
- const result = await kvStore.upsert(deviceId, key, value, creatorIp);
+ const result = await kvStore.upsert(deviceId, key, value, creatorIp);
- // 广播单个键的变更
- const uuid = res.locals.device?.uuid;
- if (uuid) {
- broadcastKeyChanged(uuid, {
- key: result.key,
- action: "upsert",
- created: result.createdAt.getTime() === result.updatedAt.getTime(),
- updatedAt: result.updatedAt,
- });
- }
+ // 广播单个键的变更
+ const uuid = res.locals.device?.uuid;
+ if (uuid) {
+ broadcastKeyChanged(uuid, {
+ key: result.key,
+ action: "upsert",
+ created: result.createdAt.getTime() === result.updatedAt.getTime(),
+ updatedAt: result.updatedAt,
+ });
+ }
- return res.status(200).json({
- deviceId: result.deviceId,
- key: result.key,
- created: result.createdAt.getTime() === result.updatedAt.getTime(),
- updatedAt: result.updatedAt,
- });
- })
+ return res.status(200).json({
+ deviceId: result.deviceId,
+ key: result.key,
+ created: result.createdAt.getTime() === result.updatedAt.getTime(),
+ updatedAt: result.updatedAt,
+ });
+ })
);
/**
@@ -394,38 +395,38 @@ router.post(
* 删除键值对
*/
router.delete(
- "/:key",
- tokenDeleteLimiter,
- errors.catchAsync(async (req, res, next) => {
- // 检查token是否为只读
- if (res.locals.appInstall?.isReadOnly) {
- return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
- }
+ "/:key",
+ tokenDeleteLimiter,
+ errors.catchAsync(async (req, res, next) => {
+ // 检查token是否为只读
+ if (res.locals.appInstall?.isReadOnly) {
+ return next(errors.createError(403, "当前token为只读模式,无法修改数据"));
+ }
- const deviceId = res.locals.deviceId;
- const { key } = req.params;
+ const deviceId = res.locals.deviceId;
+ const {key} = req.params;
- const result = await kvStore.delete(deviceId, key);
+ const result = await kvStore.delete(deviceId, key);
- if (!result) {
- return next(
- errors.createError(404, `未找到键名为 '${key}' 的记录`)
- );
- }
+ if (!result) {
+ return next(
+ errors.createError(404, `未找到键名为 '${key}' 的记录`)
+ );
+ }
- // 广播删除
- const uuid = res.locals.device?.uuid;
- if (uuid) {
- broadcastKeyChanged(uuid, {
- key,
- action: "delete",
- deletedAt: new Date(),
- });
- }
+ // 广播删除
+ const uuid = res.locals.device?.uuid;
+ if (uuid) {
+ broadcastKeyChanged(uuid, {
+ key,
+ action: "delete",
+ deletedAt: new Date(),
+ });
+ }
- // 204状态码表示成功但无内容返回
- return res.status(204).end();
- })
+ // 204状态码表示成功但无内容返回
+ return res.status(204).end();
+ })
);
export default router;
\ No newline at end of file
diff --git a/utils/config.js b/utils/config.js
index e563270..1a6e97b 100644
--- a/utils/config.js
+++ b/utils/config.js
@@ -1,4 +1,5 @@
import dotenv from "dotenv";
+
dotenv.config();
export const siteKey = process.env.SITE_KEY || "";
diff --git a/utils/crypto.js b/utils/crypto.js
index 99972de..74c43c2 100644
--- a/utils/crypto.js
+++ b/utils/crypto.js
@@ -1,5 +1,5 @@
import bcrypt from "bcrypt";
-import { Base64 } from "js-base64";
+import {Base64} from "js-base64";
const SALT_ROUNDS = 8;
@@ -7,37 +7,37 @@ const SALT_ROUNDS = 8;
* 从 base64 解码字符串
*/
export function decodeBase64(str) {
- if (!str) return null;
- try {
- return Base64.decode(str);
- } catch (error) {
- return null;
- }
+ if (!str) return null;
+ try {
+ return Base64.decode(str);
+ } catch (error) {
+ return null;
+ }
}
/**
* 对字符串进行 UTF-8 编码处理
*/
function encodeUTF8(str) {
- try {
- return encodeURIComponent(str);
- } catch (error) {
- return null;
- }
+ try {
+ return encodeURIComponent(str);
+ } catch (error) {
+ return null;
+ }
}
/**
* 验证站点密钥
*/
export function verifySiteKey(providedKey, actualKey) {
- if (!actualKey) return true; // 如果没有设置站点密钥,则总是通过
- if (!providedKey) return false;
- const decodedKey = decodeBase64(providedKey);
- if (!decodedKey) return false;
- const encodedKey = encodeUTF8(decodedKey);
- if (!encodedKey) return false;
- console.debug(encodedKey);
- return encodedKey === actualKey;
+ if (!actualKey) return true; // 如果没有设置站点密钥,则总是通过
+ if (!providedKey) return false;
+ const decodedKey = decodeBase64(providedKey);
+ if (!decodedKey) return false;
+ const encodedKey = encodeUTF8(decodedKey);
+ if (!encodedKey) return false;
+ console.debug(encodedKey);
+ return encodedKey === actualKey;
}
/**
@@ -46,8 +46,8 @@ export function verifySiteKey(providedKey, actualKey) {
* @returns {Promise} 哈希后的密码
*/
export async function hashPassword(password) {
- if (!password) return null;
- return await bcrypt.hash(password, SALT_ROUNDS);
+ if (!password) return null;
+ return await bcrypt.hash(password, SALT_ROUNDS);
}
/**
@@ -57,11 +57,11 @@ export async function hashPassword(password) {
* @returns {Promise} 密码是否匹配
*/
export async function verifyDevicePassword(providedPassword, hashedPassword) {
- if (!providedPassword || !hashedPassword) return false;
- try {
- return await bcrypt.compare(providedPassword, hashedPassword);
- } catch (error) {
- console.error('密码验证错误:', error);
- return false;
- }
+ if (!providedPassword || !hashedPassword) return false;
+ try {
+ return await bcrypt.compare(providedPassword, hashedPassword);
+ } catch (error) {
+ console.error('密码验证错误:', error);
+ return false;
+ }
}
diff --git a/utils/deviceCodeStore.js b/utils/deviceCodeStore.js
index f16795e..5e5c173 100644
--- a/utils/deviceCodeStore.js
+++ b/utils/deviceCodeStore.js
@@ -6,182 +6,182 @@
*/
class DeviceCodeStore {
- constructor() {
- // 存储结构: { deviceCode: { token: string, expiresAt: number, createdAt: number } }
- this.store = new Map();
+ constructor() {
+ // 存储结构: { deviceCode: { token: string, expiresAt: number, createdAt: number } }
+ this.store = new Map();
- // 默认过期时间: 15分钟
- this.expirationTime = 15 * 60 * 1000;
+ // 默认过期时间: 15分钟
+ this.expirationTime = 15 * 60 * 1000;
- // 定期清理过期数据 (每5分钟)
- this.cleanupInterval = setInterval(() => {
- this.cleanup();
- }, 5 * 60 * 1000);
- }
-
- /**
- * 生成设备代码 (格式: 1234-ABCD)
- */
- generateDeviceCode() {
- const part1 = Math.floor(1000 + Math.random() * 9000).toString(); // 4位数字
- const part2 = Math.random().toString(36).substring(2, 6).toUpperCase(); // 4位字母
- return `${part1}-${part2}`;
- }
-
- /**
- * 创建新的设备代码
- * @returns {string} 生成的设备代码
- */
- create() {
- let deviceCode;
-
- // 确保生成的代码不重复
- do {
- deviceCode = this.generateDeviceCode();
- } while (this.store.has(deviceCode));
-
- const now = Date.now();
- this.store.set(deviceCode, {
- token: null,
- expiresAt: now + this.expirationTime,
- createdAt: now,
- });
-
- return deviceCode;
- }
-
- /**
- * 绑定令牌到设备代码
- * @param {string} deviceCode - 设备代码
- * @param {string} token - 令牌
- * @returns {boolean} 是否成功绑定
- */
- bindToken(deviceCode, token) {
- const entry = this.store.get(deviceCode);
-
- if (!entry) {
- return false;
+ // 定期清理过期数据 (每5分钟)
+ this.cleanupInterval = setInterval(() => {
+ this.cleanup();
+ }, 5 * 60 * 1000);
}
- // 检查是否过期
- if (Date.now() > entry.expiresAt) {
- this.store.delete(deviceCode);
- return false;
+ /**
+ * 生成设备代码 (格式: 1234-ABCD)
+ */
+ generateDeviceCode() {
+ const part1 = Math.floor(1000 + Math.random() * 9000).toString(); // 4位数字
+ const part2 = Math.random().toString(36).substring(2, 6).toUpperCase(); // 4位字母
+ return `${part1}-${part2}`;
}
- // 绑定令牌
- entry.token = token;
- return true;
- }
+ /**
+ * 创建新的设备代码
+ * @returns {string} 生成的设备代码
+ */
+ create() {
+ let deviceCode;
- /**
- * 获取设备代码对应的令牌(获取后删除)
- * @param {string} deviceCode - 设备代码
- * @returns {string|null} 令牌,如果不存在或未绑定返回null
- */
- getAndRemove(deviceCode) {
- const entry = this.store.get(deviceCode);
+ // 确保生成的代码不重复
+ do {
+ deviceCode = this.generateDeviceCode();
+ } while (this.store.has(deviceCode));
- if (!entry) {
- return null;
+ const now = Date.now();
+ this.store.set(deviceCode, {
+ token: null,
+ expiresAt: now + this.expirationTime,
+ createdAt: now,
+ });
+
+ return deviceCode;
}
- // 检查是否过期
- if (Date.now() > entry.expiresAt) {
- this.store.delete(deviceCode);
- return null;
+ /**
+ * 绑定令牌到设备代码
+ * @param {string} deviceCode - 设备代码
+ * @param {string} token - 令牌
+ * @returns {boolean} 是否成功绑定
+ */
+ bindToken(deviceCode, token) {
+ const entry = this.store.get(deviceCode);
+
+ if (!entry) {
+ return false;
+ }
+
+ // 检查是否过期
+ if (Date.now() > entry.expiresAt) {
+ this.store.delete(deviceCode);
+ return false;
+ }
+
+ // 绑定令牌
+ entry.token = token;
+ return true;
}
- // 如果令牌未绑定,返回null但不删除代码
- if (!entry.token) {
- return null;
- }
+ /**
+ * 获取设备代码对应的令牌(获取后删除)
+ * @param {string} deviceCode - 设备代码
+ * @returns {string|null} 令牌,如果不存在或未绑定返回null
+ */
+ getAndRemove(deviceCode) {
+ const entry = this.store.get(deviceCode);
- // 获取令牌后删除条目
- const token = entry.token;
- this.store.delete(deviceCode);
- return token;
- }
+ if (!entry) {
+ return null;
+ }
- /**
- * 检查设备代码是否存在且未过期
- * @param {string} deviceCode - 设备代码
- * @returns {boolean}
- */
- exists(deviceCode) {
- const entry = this.store.get(deviceCode);
+ // 检查是否过期
+ if (Date.now() > entry.expiresAt) {
+ this.store.delete(deviceCode);
+ return null;
+ }
- if (!entry) {
- return false;
- }
+ // 如果令牌未绑定,返回null但不删除代码
+ if (!entry.token) {
+ return null;
+ }
- if (Date.now() > entry.expiresAt) {
- this.store.delete(deviceCode);
- return false;
- }
-
- return true;
- }
-
- /**
- * 获取设备代码的状态信息(不删除)
- * @param {string} deviceCode - 设备代码
- * @returns {object|null} 状态信息
- */
- getStatus(deviceCode) {
- const entry = this.store.get(deviceCode);
-
- if (!entry) {
- return null;
- }
-
- if (Date.now() > entry.expiresAt) {
- this.store.delete(deviceCode);
- return null;
- }
-
- return {
- hasToken: !!entry.token,
- expiresAt: entry.expiresAt,
- createdAt: entry.createdAt,
- };
- }
-
- /**
- * 清理过期的条目
- */
- cleanup() {
- const now = Date.now();
- let cleanedCount = 0;
-
- for (const [deviceCode, entry] of this.store.entries()) {
- if (now > entry.expiresAt) {
+ // 获取令牌后删除条目
+ const token = entry.token;
this.store.delete(deviceCode);
- cleanedCount++;
- }
+ return token;
}
- if (cleanedCount > 0) {
- console.log(`清理了 ${cleanedCount} 个过期的设备代码`);
- }
- }
+ /**
+ * 检查设备代码是否存在且未过期
+ * @param {string} deviceCode - 设备代码
+ * @returns {boolean}
+ */
+ exists(deviceCode) {
+ const entry = this.store.get(deviceCode);
- /**
- * 获取当前存储的条目数量
- */
- size() {
- return this.store.size;
- }
+ if (!entry) {
+ return false;
+ }
- /**
- * 清理定时器(用于优雅关闭)
- */
- destroy() {
- if (this.cleanupInterval) {
- clearInterval(this.cleanupInterval);
+ if (Date.now() > entry.expiresAt) {
+ this.store.delete(deviceCode);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 获取设备代码的状态信息(不删除)
+ * @param {string} deviceCode - 设备代码
+ * @returns {object|null} 状态信息
+ */
+ getStatus(deviceCode) {
+ const entry = this.store.get(deviceCode);
+
+ if (!entry) {
+ return null;
+ }
+
+ if (Date.now() > entry.expiresAt) {
+ this.store.delete(deviceCode);
+ return null;
+ }
+
+ return {
+ hasToken: !!entry.token,
+ expiresAt: entry.expiresAt,
+ createdAt: entry.createdAt,
+ };
+ }
+
+ /**
+ * 清理过期的条目
+ */
+ cleanup() {
+ const now = Date.now();
+ let cleanedCount = 0;
+
+ for (const [deviceCode, entry] of this.store.entries()) {
+ if (now > entry.expiresAt) {
+ this.store.delete(deviceCode);
+ cleanedCount++;
+ }
+ }
+
+ if (cleanedCount > 0) {
+ console.log(`清理了 ${cleanedCount} 个过期的设备代码`);
+ }
+ }
+
+ /**
+ * 获取当前存储的条目数量
+ */
+ size() {
+ return this.store.size;
+ }
+
+ /**
+ * 清理定时器(用于优雅关闭)
+ */
+ destroy() {
+ if (this.cleanupInterval) {
+ clearInterval(this.cleanupInterval);
+ }
+ this.store.clear();
}
- this.store.clear();
- }
}
// 导出单例
@@ -189,11 +189,11 @@ const deviceCodeStore = new DeviceCodeStore();
// 优雅关闭处理
process.on('SIGTERM', () => {
- deviceCodeStore.destroy();
+ deviceCodeStore.destroy();
});
process.on('SIGINT', () => {
- deviceCodeStore.destroy();
+ deviceCodeStore.destroy();
});
export default deviceCodeStore;
diff --git a/utils/errors.js b/utils/errors.js
index 1e96af4..2118e97 100644
--- a/utils/errors.js
+++ b/utils/errors.js
@@ -7,14 +7,14 @@
* @returns {object} 标准错误对象
*/
const createError = (statusCode, message, details = null, code = null) => {
- // 直接返回错误对象,不抛出异常
- const error = {
- statusCode: statusCode,
- message: message || '服务器错误',
- details: details,
- code: code || undefined,
- };
- return error;
+ // 直接返回错误对象,不抛出异常
+ const error = {
+ statusCode: statusCode,
+ message: message || '服务器错误',
+ details: details,
+ code: code || undefined,
+ };
+ return error;
};
/**
@@ -24,11 +24,11 @@ const createError = (statusCode, message, details = null, code = null) => {
* @returns {object} 格式化的成功响应
*/
const createSuccessResponse = (data, message = null) => {
- return {
- success: true,
- message,
- data,
- };
+ return {
+ success: true,
+ message,
+ data,
+ };
};
/**
@@ -37,20 +37,20 @@ const createSuccessResponse = (data, message = null) => {
* @param {Function} next - Express中间件next函数
*/
const passError = (error, next) => {
- // 不管是什么类型的错误,统一转换并传递
- if (error instanceof Error) {
- // 如果是标准Error,则转换为HTTP错误并保留原始信息
- const httpError = {
- statusCode: error.statusCode || 500,
- message: error.message || '服务器错误',
- details: error.details || null,
- originalError: error
- };
- next(httpError);
- } else {
- // 已经是自定义错误对象结构,直接传递
- next(error);
- }
+ // 不管是什么类型的错误,统一转换并传递
+ if (error instanceof Error) {
+ // 如果是标准Error,则转换为HTTP错误并保留原始信息
+ const httpError = {
+ statusCode: error.statusCode || 500,
+ message: error.message || '服务器错误',
+ details: error.details || null,
+ originalError: error
+ };
+ next(httpError);
+ } else {
+ // 已经是自定义错误对象结构,直接传递
+ next(error);
+ }
};
/**
@@ -59,11 +59,11 @@ const passError = (error, next) => {
* @returns {Function} 包装后的函数
*/
const catchAsync = (fn) => {
- return (req, res, next) => {
- Promise.resolve(fn(req, res, next)).catch(error => {
- passError(error, next);
- });
- };
+ return (req, res, next) => {
+ Promise.resolve(fn(req, res, next)).catch(error => {
+ passError(error, next);
+ });
+ };
};
/**
@@ -73,31 +73,31 @@ const catchAsync = (fn) => {
* @returns {[error, result]} 包含错误和结果的数组
*/
const trySafe = (fn, ...args) => {
- try {
- const result = fn(...args);
- return [null, result];
- } catch (error) {
- return [error, null];
- }
+ try {
+ const result = fn(...args);
+ return [null, result];
+ } catch (error) {
+ return [error, null];
+ }
};
// 常用状态码
const HTTP_STATUS = {
- OK: 200,
- CREATED: 201,
- NO_CONTENT: 204,
- BAD_REQUEST: 400,
- UNAUTHORIZED: 401,
- FORBIDDEN: 403,
- NOT_FOUND: 404,
- INTERNAL_SERVER_ERROR: 500,
+ OK: 200,
+ CREATED: 201,
+ NO_CONTENT: 204,
+ BAD_REQUEST: 400,
+ UNAUTHORIZED: 401,
+ FORBIDDEN: 403,
+ NOT_FOUND: 404,
+ INTERNAL_SERVER_ERROR: 500,
};
export default {
- createError,
- createSuccessResponse,
- passError,
- catchAsync,
- trySafe,
- HTTP_STATUS,
+ createError,
+ createSuccessResponse,
+ passError,
+ catchAsync,
+ trySafe,
+ HTTP_STATUS,
};
diff --git a/utils/instrumentation.js b/utils/instrumentation.js
index a5a9a1e..51e3c33 100644
--- a/utils/instrumentation.js
+++ b/utils/instrumentation.js
@@ -1,42 +1,43 @@
import "dotenv/config";
-import { NodeSDK } from "@opentelemetry/sdk-node";
-import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
-import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
-import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
-import { resourceFromAttributes } from "@opentelemetry/resources";
-import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
+import {NodeSDK} from "@opentelemetry/sdk-node";
+import {getNodeAutoInstrumentations} from "@opentelemetry/auto-instrumentations-node";
+import {OTLPTraceExporter} from "@opentelemetry/exporter-trace-otlp-proto";
+import {BatchSpanProcessor} from "@opentelemetry/sdk-trace-base";
+import {resourceFromAttributes} from "@opentelemetry/resources";
+import {SemanticResourceAttributes} from "@opentelemetry/semantic-conventions";
+
if (process.env.AXIOM_TOKEN && process.env.AXIOM_DATASET) {
- // Initialize OTLP trace exporter with the endpoint URL and headers
- // Initialize OTLP trace exporter with the endpoint URL and headers
- const traceExporter = new OTLPTraceExporter({
- url: "https://api.axiom.co/v1/traces",
- headers: {
- Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
- "X-Axiom-Dataset": process.env.AXIOM_DATASET,
- },
- });
+ // Initialize OTLP trace exporter with the endpoint URL and headers
+ // Initialize OTLP trace exporter with the endpoint URL and headers
+ const traceExporter = new OTLPTraceExporter({
+ url: "https://api.axiom.co/v1/traces",
+ headers: {
+ Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
+ "X-Axiom-Dataset": process.env.AXIOM_DATASET,
+ },
+ });
- const resourceAttributes = {
- [SemanticResourceAttributes.SERVICE_NAME]: "node traces",
- };
+ const resourceAttributes = {
+ [SemanticResourceAttributes.SERVICE_NAME]: "node traces",
+ };
- const resource = resourceFromAttributes(resourceAttributes);
+ const resource = resourceFromAttributes(resourceAttributes);
- // Configuring the OpenTelemetry Node SDK
- const sdk = new NodeSDK({
- // Adding a BatchSpanProcessor to batch and send traces
- spanProcessor: new BatchSpanProcessor(traceExporter),
+ // Configuring the OpenTelemetry Node SDK
+ const sdk = new NodeSDK({
+ // Adding a BatchSpanProcessor to batch and send traces
+ spanProcessor: new BatchSpanProcessor(traceExporter),
- // Registering the resource to the SDK
- resource: resource,
+ // Registering the resource to the SDK
+ resource: resource,
- // Adding auto-instrumentations to automatically collect trace data
- instrumentations: [getNodeAutoInstrumentations()],
- });
+ // Adding auto-instrumentations to automatically collect trace data
+ instrumentations: [getNodeAutoInstrumentations()],
+ });
- console.log("✅成功加载 Axiom 遥测");
- // Starting the OpenTelemetry SDK to begin collecting telemetry data
- sdk.start();
+ console.log("✅成功加载 Axiom 遥测");
+ // Starting the OpenTelemetry SDK to begin collecting telemetry data
+ sdk.start();
} else {
- console.log("❌未设置 Axiom 遥测");
+ console.log("❌未设置 Axiom 遥测");
}
diff --git a/utils/jwt.js b/utils/jwt.js
index 4349968..ed2da2e 100644
--- a/utils/jwt.js
+++ b/utils/jwt.js
@@ -1,11 +1,11 @@
import jwt from 'jsonwebtoken';
import {
- generateAccessToken,
- verifyAccessToken,
- generateTokenPair,
- refreshAccessToken,
- revokeAllTokens,
- revokeRefreshToken,
+ generateAccessToken,
+ generateTokenPair,
+ refreshAccessToken,
+ revokeAllTokens,
+ revokeRefreshToken,
+ verifyAccessToken,
} from './tokenManager.js';
// JWT 配置(支持 HS256 与 RS256)
@@ -20,14 +20,14 @@ const JWT_PRIVATE_KEY = process.env.JWT_PRIVATE_KEY?.replace(/\\n/g, '\n');
const JWT_PUBLIC_KEY = process.env.JWT_PUBLIC_KEY?.replace(/\\n/g, '\n');
function getSignVerifyKeys() {
- if (JWT_ALG === 'RS256') {
- if (!JWT_PRIVATE_KEY || !JWT_PUBLIC_KEY) {
- throw new Error('RS256 需要同时提供 JWT_PRIVATE_KEY 与 JWT_PUBLIC_KEY');
+ if (JWT_ALG === 'RS256') {
+ if (!JWT_PRIVATE_KEY || !JWT_PUBLIC_KEY) {
+ throw new Error('RS256 需要同时提供 JWT_PRIVATE_KEY 与 JWT_PUBLIC_KEY');
+ }
+ return {signKey: JWT_PRIVATE_KEY, verifyKey: JWT_PUBLIC_KEY};
}
- return { signKey: JWT_PRIVATE_KEY, verifyKey: JWT_PUBLIC_KEY };
- }
- // 默认 HS256
- return { signKey: JWT_SECRET, verifyKey: JWT_SECRET };
+ // 默认 HS256
+ return {signKey: JWT_SECRET, verifyKey: JWT_SECRET};
}
/**
@@ -35,11 +35,11 @@ function getSignVerifyKeys() {
* @deprecated 建议使用 generateAccessToken
*/
export function signToken(payload) {
- const { signKey } = getSignVerifyKeys();
- return jwt.sign(payload, signKey, {
- expiresIn: JWT_EXPIRES_IN,
- algorithm: JWT_ALG,
- });
+ const {signKey} = getSignVerifyKeys();
+ return jwt.sign(payload, signKey, {
+ expiresIn: JWT_EXPIRES_IN,
+ algorithm: JWT_ALG,
+ });
}
/**
@@ -47,8 +47,8 @@ export function signToken(payload) {
* @deprecated 建议使用 verifyAccessToken
*/
export function verifyToken(token) {
- const { verifyKey } = getSignVerifyKeys();
- return jwt.verify(token, verifyKey, { algorithms: [JWT_ALG] });
+ const {verifyKey} = getSignVerifyKeys();
+ return jwt.verify(token, verifyKey, {algorithms: [JWT_ALG]});
}
/**
@@ -56,21 +56,21 @@ export function verifyToken(token) {
* @deprecated 建议使用 generateTokenPair 获取完整的令牌对
*/
export function generateAccountToken(account) {
- return signToken({
- accountId: account.id,
- provider: account.provider,
- email: account.email,
- name: account.name,
- avatarUrl: account.avatarUrl,
- });
+ return signToken({
+ accountId: account.id,
+ provider: account.provider,
+ email: account.email,
+ name: account.name,
+ avatarUrl: account.avatarUrl,
+ });
}
// 重新导出新的token管理功能
export {
- generateAccessToken,
- verifyAccessToken,
- generateTokenPair,
- refreshAccessToken,
- revokeAllTokens,
- revokeRefreshToken,
+ generateAccessToken,
+ verifyAccessToken,
+ generateTokenPair,
+ refreshAccessToken,
+ revokeAllTokens,
+ revokeRefreshToken,
};
\ No newline at end of file
diff --git a/utils/kvStore.js b/utils/kvStore.js
index 8179c4b..f521f36 100644
--- a/utils/kvStore.js
+++ b/utils/kvStore.js
@@ -1,223 +1,224 @@
-import { PrismaClient } from "@prisma/client";
-import { keysTotal } from "./metrics.js";
+import {PrismaClient} from "@prisma/client";
+import {keysTotal} from "./metrics.js";
const prisma = new PrismaClient();
+
class KVStore {
- /**
- * 通过设备ID和键名获取值
- * @param {number} deviceId - 设备ID
- * @param {string} key - 键名
- * @returns {object|null} 键对应的值或null
- */
- async get(deviceId, key) {
- const item = await prisma.kVStore.findUnique({
- where: {
- deviceId_key: {
- deviceId: deviceId,
- key: key,
- },
- },
- });
- return item ? item.value : null;
- }
-
- /**
- * 获取键的完整信息(包括元数据)
- * @param {number} deviceId - 设备ID
- * @param {string} key - 键名
- * @returns {object|null} 键的完整信息或null
- */
- async getMetadata(deviceId, key) {
- const item = await prisma.kVStore.findUnique({
- where: {
- deviceId_key: {
- deviceId: deviceId,
- key: key,
- },
- },
- select: {
- key: true,
- deviceId: true,
- creatorIp: true,
- createdAt: true,
- updatedAt: true,
- },
- });
-
- if (!item) return null;
-
- // 转换为更友好的格式
- return {
- deviceId: item.deviceId,
- key: item.key,
- metadata: {
- creatorIp: item.creatorIp,
- createdAt: item.createdAt,
- updatedAt: item.updatedAt,
- },
- };
- }
-
- /**
- * 在指定设备下创建或更新键值
- * @param {number} deviceId - 设备ID
- * @param {string} key - 键名
- * @param {object} value - 键值
- * @param {string} creatorIp - 创建者IP,可选
- * @returns {object} 创建或更新的记录
- */
- async upsert(deviceId, key, value, creatorIp = "") {
- const item = await prisma.kVStore.upsert({
- where: {
- deviceId_key: {
- deviceId: deviceId,
- key: key,
- },
- },
- update: {
- value,
- ...(creatorIp && { creatorIp }),
- },
- create: {
- deviceId: deviceId,
- key: key,
- value,
- creatorIp,
- },
- });
-
- // 更新键总数指标
- const totalKeys = await prisma.kVStore.count();
- keysTotal.set(totalKeys);
-
- // 返回带有设备ID和原始键的结果
- return {
- deviceId,
- key,
- value: item.value,
- creatorIp: item.creatorIp,
- createdAt: item.createdAt,
- updatedAt: item.updatedAt,
- };
- }
-
- /**
- * 通过设备ID和键名删除
- * @param {number} deviceId - 设备ID
- * @param {string} key - 键名
- * @returns {object|null} 删除的记录或null
- */
- async delete(deviceId, key) {
- try {
- const item = await prisma.kVStore.delete({
- where: {
- deviceId_key: {
- deviceId: deviceId,
- key: key,
- },
- },
- });
-
- // 更新键总数指标
- const totalKeys = await prisma.kVStore.count();
- keysTotal.set(totalKeys);
-
- return item ? { ...item, deviceId, key } : null;
- } catch (error) {
- // 忽略记录不存在的错误
- if (error.code === "P2025") {
- return null;
- }
- throw error;
+ /**
+ * 通过设备ID和键名获取值
+ * @param {number} deviceId - 设备ID
+ * @param {string} key - 键名
+ * @returns {object|null} 键对应的值或null
+ */
+ async get(deviceId, key) {
+ const item = await prisma.kVStore.findUnique({
+ where: {
+ deviceId_key: {
+ deviceId: deviceId,
+ key: key,
+ },
+ },
+ });
+ return item ? item.value : null;
}
- }
- /**
- * 列出指定设备下的所有键名及其元数据
- * @param {number} deviceId - 设备ID
- * @param {object} options - 选项参数
- * @returns {Array} 键名和元数据数组
- */
- async list(deviceId, options = {}) {
- const { sortBy = "key", sortDir = "asc", limit = 100, skip = 0 } = options;
+ /**
+ * 获取键的完整信息(包括元数据)
+ * @param {number} deviceId - 设备ID
+ * @param {string} key - 键名
+ * @returns {object|null} 键的完整信息或null
+ */
+ async getMetadata(deviceId, key) {
+ const item = await prisma.kVStore.findUnique({
+ where: {
+ deviceId_key: {
+ deviceId: deviceId,
+ key: key,
+ },
+ },
+ select: {
+ key: true,
+ deviceId: true,
+ creatorIp: true,
+ createdAt: true,
+ updatedAt: true,
+ },
+ });
- // 构建排序条件
- const orderBy = {};
- orderBy[sortBy] = sortDir.toLowerCase();
+ if (!item) return null;
- // 查询设备的所有键
- const items = await prisma.kVStore.findMany({
- where: {
- deviceId: deviceId,
- },
- select: {
- deviceId: true,
- key: true,
- creatorIp: true,
- createdAt: true,
- updatedAt: true,
- value: false,
- },
- orderBy,
- take: limit,
- skip: skip,
- });
+ // 转换为更友好的格式
+ return {
+ deviceId: item.deviceId,
+ key: item.key,
+ metadata: {
+ creatorIp: item.creatorIp,
+ createdAt: item.createdAt,
+ updatedAt: item.updatedAt,
+ },
+ };
+ }
- // 处理结果
- return items.map((item) => ({
- deviceId: item.deviceId,
- key: item.key,
- metadata: {
- creatorIp: item.creatorIp,
- createdAt: item.createdAt,
- updatedAt: item.updatedAt,
- },
- }));
- }
+ /**
+ * 在指定设备下创建或更新键值
+ * @param {number} deviceId - 设备ID
+ * @param {string} key - 键名
+ * @param {object} value - 键值
+ * @param {string} creatorIp - 创建者IP,可选
+ * @returns {object} 创建或更新的记录
+ */
+ async upsert(deviceId, key, value, creatorIp = "") {
+ const item = await prisma.kVStore.upsert({
+ where: {
+ deviceId_key: {
+ deviceId: deviceId,
+ key: key,
+ },
+ },
+ update: {
+ value,
+ ...(creatorIp && {creatorIp}),
+ },
+ create: {
+ deviceId: deviceId,
+ key: key,
+ value,
+ creatorIp,
+ },
+ });
- /**
- * 获取指定设备下的键名列表(不包括内容)
- * @param {number} deviceId - 设备ID
- * @param {object} options - 查询选项
- * @returns {Array} 键名列表
- */
- async listKeysOnly(deviceId, options = {}) {
- const { sortBy = "key", sortDir = "asc", limit = 100, skip = 0 } = options;
+ // 更新键总数指标
+ const totalKeys = await prisma.kVStore.count();
+ keysTotal.set(totalKeys);
- // 构建排序条件
- const orderBy = {};
- orderBy[sortBy] = sortDir.toLowerCase();
+ // 返回带有设备ID和原始键的结果
+ return {
+ deviceId,
+ key,
+ value: item.value,
+ creatorIp: item.creatorIp,
+ createdAt: item.createdAt,
+ updatedAt: item.updatedAt,
+ };
+ }
- // 查询设备的所有键,只选择键名
- const items = await prisma.kVStore.findMany({
- where: {
- deviceId: deviceId,
- },
- select: {
- key: true,
- },
- orderBy,
- take: limit,
- skip: skip,
- });
+ /**
+ * 通过设备ID和键名删除
+ * @param {number} deviceId - 设备ID
+ * @param {string} key - 键名
+ * @returns {object|null} 删除的记录或null
+ */
+ async delete(deviceId, key) {
+ try {
+ const item = await prisma.kVStore.delete({
+ where: {
+ deviceId_key: {
+ deviceId: deviceId,
+ key: key,
+ },
+ },
+ });
- // 只返回键名数组
- return items.map((item) => item.key);
- }
+ // 更新键总数指标
+ const totalKeys = await prisma.kVStore.count();
+ keysTotal.set(totalKeys);
- /**
- * 统计指定设备下的键值对数量
- * @param {number} deviceId - 设备ID
- * @returns {number} 键值对数量
- */
- async count(deviceId) {
- const count = await prisma.kVStore.count({
- where: {
- deviceId: deviceId,
- },
- });
- return count;
- }
+ return item ? {...item, deviceId, key} : null;
+ } catch (error) {
+ // 忽略记录不存在的错误
+ if (error.code === "P2025") {
+ return null;
+ }
+ throw error;
+ }
+ }
+
+ /**
+ * 列出指定设备下的所有键名及其元数据
+ * @param {number} deviceId - 设备ID
+ * @param {object} options - 选项参数
+ * @returns {Array} 键名和元数据数组
+ */
+ async list(deviceId, options = {}) {
+ const {sortBy = "key", sortDir = "asc", limit = 100, skip = 0} = options;
+
+ // 构建排序条件
+ const orderBy = {};
+ orderBy[sortBy] = sortDir.toLowerCase();
+
+ // 查询设备的所有键
+ const items = await prisma.kVStore.findMany({
+ where: {
+ deviceId: deviceId,
+ },
+ select: {
+ deviceId: true,
+ key: true,
+ creatorIp: true,
+ createdAt: true,
+ updatedAt: true,
+ value: false,
+ },
+ orderBy,
+ take: limit,
+ skip: skip,
+ });
+
+ // 处理结果
+ return items.map((item) => ({
+ deviceId: item.deviceId,
+ key: item.key,
+ metadata: {
+ creatorIp: item.creatorIp,
+ createdAt: item.createdAt,
+ updatedAt: item.updatedAt,
+ },
+ }));
+ }
+
+ /**
+ * 获取指定设备下的键名列表(不包括内容)
+ * @param {number} deviceId - 设备ID
+ * @param {object} options - 查询选项
+ * @returns {Array} 键名列表
+ */
+ async listKeysOnly(deviceId, options = {}) {
+ const {sortBy = "key", sortDir = "asc", limit = 100, skip = 0} = options;
+
+ // 构建排序条件
+ const orderBy = {};
+ orderBy[sortBy] = sortDir.toLowerCase();
+
+ // 查询设备的所有键,只选择键名
+ const items = await prisma.kVStore.findMany({
+ where: {
+ deviceId: deviceId,
+ },
+ select: {
+ key: true,
+ },
+ orderBy,
+ take: limit,
+ skip: skip,
+ });
+
+ // 只返回键名数组
+ return items.map((item) => item.key);
+ }
+
+ /**
+ * 统计指定设备下的键值对数量
+ * @param {number} deviceId - 设备ID
+ * @returns {number} 键值对数量
+ */
+ async count(deviceId) {
+ const count = await prisma.kVStore.count({
+ where: {
+ deviceId: deviceId,
+ },
+ });
+ return count;
+ }
}
export default new KVStore();
diff --git a/utils/metrics.js b/utils/metrics.js
index 8354612..104a137 100644
--- a/utils/metrics.js
+++ b/utils/metrics.js
@@ -5,45 +5,45 @@ const register = new client.Registry();
// 当前在线设备数(连接了 SocketIO 的设备)
export const onlineDevicesGauge = new client.Gauge({
- name: 'classworks_online_devices_total',
- help: 'Total number of online devices (connected via SocketIO)',
- registers: [register],
+ name: 'classworks_online_devices_total',
+ help: 'Total number of online devices (connected via SocketIO)',
+ registers: [register],
});
// 已注册设备总数
export const registeredDevicesTotal = new client.Gauge({
- name: 'classworks_registered_devices_total',
- help: 'Total number of registered devices',
- registers: [register],
+ name: 'classworks_registered_devices_total',
+ help: 'Total number of registered devices',
+ registers: [register],
});
// 已创建键总数(不区分设备)
export const keysTotal = new client.Gauge({
- name: 'classworks_keys_total',
- help: 'Total number of keys across all devices',
- registers: [register],
+ name: 'classworks_keys_total',
+ help: 'Total number of keys across all devices',
+ registers: [register],
});
// 初始化指标数据
export async function initializeMetrics() {
- try {
- const { PrismaClient } = await import('@prisma/client');
- const prisma = new PrismaClient();
+ try {
+ const {PrismaClient} = await import('@prisma/client');
+ const prisma = new PrismaClient();
- // 获取已注册设备总数
- const deviceCount = await prisma.device.count();
- registeredDevicesTotal.set(deviceCount);
+ // 获取已注册设备总数
+ const deviceCount = await prisma.device.count();
+ registeredDevicesTotal.set(deviceCount);
- // 获取已创建键总数
- const keyCount = await prisma.kVStore.count();
- keysTotal.set(keyCount);
+ // 获取已创建键总数
+ 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);
- }
+ await prisma.$disconnect();
+ console.log('Prometheus metrics initialized - Devices:', deviceCount, 'Keys:', keyCount);
+ } catch (error) {
+ console.error('Failed to initialize metrics:', error);
+ }
}
// 导出注册表用于 /metrics 端点
-export { register };
\ No newline at end of file
+export {register};
\ No newline at end of file
diff --git a/utils/siteinfo.js b/utils/siteinfo.js
index b0dab1e..c5bd628 100644
--- a/utils/siteinfo.js
+++ b/utils/siteinfo.js
@@ -1,4 +1,4 @@
-import { PrismaClient } from "@prisma/client";
+import {PrismaClient} from "@prisma/client";
import kvStore from "./kvStore.js";
const prisma = new PrismaClient();
@@ -12,8 +12,8 @@ let systemDeviceId = null;
// 封装默认 readme 对象
const defaultReadme = {
- title: "Classworks 服务端",
- readme: "暂无 Readme 内容",
+ title: "Classworks 服务端",
+ readme: "暂无 Readme 内容",
};
/**
@@ -21,25 +21,25 @@ const defaultReadme = {
* @returns {Promise} 系统设备ID
*/
async function getSystemDeviceId() {
- if (systemDeviceId) return systemDeviceId;
+ if (systemDeviceId) return systemDeviceId;
- let device = await prisma.device.findUnique({
- where: { uuid: SYSTEM_DEVICE_UUID },
- select: { id: true },
- });
-
- if (!device) {
- device = await prisma.device.create({
- data: {
- uuid: SYSTEM_DEVICE_UUID,
- name: "系统设备",
- },
- select: { id: true },
+ let device = await prisma.device.findUnique({
+ where: {uuid: SYSTEM_DEVICE_UUID},
+ select: {id: true},
});
- }
- systemDeviceId = device.id;
- return systemDeviceId;
+ if (!device) {
+ device = await prisma.device.create({
+ data: {
+ uuid: SYSTEM_DEVICE_UUID,
+ name: "系统设备",
+ },
+ select: {id: true},
+ });
+ }
+
+ systemDeviceId = device.id;
+ return systemDeviceId;
}
/**
@@ -47,26 +47,26 @@ async function getSystemDeviceId() {
* 在应用启动时调用此函数
*/
export const initReadme = async () => {
- try {
- const deviceId = await getSystemDeviceId();
- const storedValue = await kvStore.get(deviceId, "info");
+ try {
+ const deviceId = await getSystemDeviceId();
+ const storedValue = await kvStore.get(deviceId, "info");
- // 合并默认值与存储值,确保结构完整
- readmeValue = {
- ...defaultReadme,
- ...(storedValue || {}),
- };
+ // 合并默认值与存储值,确保结构完整
+ readmeValue = {
+ ...defaultReadme,
+ ...(storedValue || {}),
+ };
- console.log("✅ 站点信息初始化成功");
- } catch (error) {
- console.error("❌ 站点信息初始化失败:", {
- message: error?.message,
- stack: error?.stack,
- });
+ console.log("✅ 站点信息初始化成功");
+ } catch (error) {
+ console.error("❌ 站点信息初始化失败:", {
+ message: error?.message,
+ stack: error?.stack,
+ });
- // 确保在异常情况下也有默认值
- readmeValue = { ...defaultReadme };
- }
+ // 确保在异常情况下也有默认值
+ readmeValue = {...defaultReadme};
+ }
};
/**
@@ -74,7 +74,7 @@ export const initReadme = async () => {
* @returns {Object} readme 值对象
*/
export const getReadmeValue = () => {
- return readmeValue || { ...defaultReadme };
+ return readmeValue || {...defaultReadme};
};
/**
@@ -83,19 +83,19 @@ export const getReadmeValue = () => {
* @returns {Promise}
*/
export const updateReadmeValue = async (newValue) => {
- try {
- const deviceId = await getSystemDeviceId();
- await kvStore.upsert(deviceId, "info", newValue);
- readmeValue = {
- ...defaultReadme,
- ...newValue,
- };
- console.log("✅ 站点信息更新成功");
- } catch (error) {
- console.error("❌ 站点信息更新失败:", {
- message: error?.message,
- stack: error?.stack,
- });
- throw error;
- }
+ try {
+ const deviceId = await getSystemDeviceId();
+ await kvStore.upsert(deviceId, "info", newValue);
+ readmeValue = {
+ ...defaultReadme,
+ ...newValue,
+ };
+ console.log("✅ 站点信息更新成功");
+ } catch (error) {
+ console.error("❌ 站点信息更新失败:", {
+ message: error?.message,
+ stack: error?.stack,
+ });
+ throw error;
+ }
};
\ No newline at end of file
diff --git a/utils/socket.js b/utils/socket.js
index 3e7f503..ee6aa41 100644
--- a/utils/socket.js
+++ b/utils/socket.js
@@ -9,9 +9,9 @@
* - 提供广播 KV 键变更的工具方法
*/
-import { Server } from "socket.io";
-import { PrismaClient } from "@prisma/client";
-import { onlineDevicesGauge } from "./metrics.js";
+import {Server} from "socket.io";
+import {PrismaClient} from "@prisma/client";
+import {onlineDevicesGauge} from "./metrics.js";
// Socket.IO 单例实例
let io = null;
@@ -27,105 +27,107 @@ const prisma = new PrismaClient();
* @param {import('http').Server} server HTTP Server 实例
*/
export function initSocket(server) {
- if (io) return io;
+ if (io) return io;
- const allowOrigin = process.env.FRONTEND_URL || "*";
+ const allowOrigin = process.env.FRONTEND_URL || "*";
- io = new Server(server, {
- cors: {
- origin: allowOrigin,
- methods: ["GET", "POST"],
- credentials: true,
- },
- });
-
- io.on("connection", (socket) => {
- // 初始化每个连接所加入的设备房间集合
- socket.data.deviceUuids = new Set();
-
- // 仅允许通过 query.token/apptoken 加入
- const qToken = socket.handshake?.query?.token || socket.handshake?.query?.apptoken;
- if (qToken && typeof qToken === "string") {
- joinByToken(socket, qToken).catch(() => {});
- }
-
- // 客户端使用 KV token 加入房间
- socket.on("join-token", (payload) => {
- const token = payload?.token || payload?.apptoken;
- if (typeof token === "string" && token.length > 0) {
- joinByToken(socket, token).catch(() => {});
- }
+ io = new Server(server, {
+ cors: {
+ origin: allowOrigin,
+ methods: ["GET", "POST"],
+ credentials: true,
+ },
});
- // 客户端使用 token 离开房间
- socket.on("leave-token", async (payload) => {
- try {
- const token = payload?.token || payload?.apptoken;
- if (typeof token !== "string" || token.length === 0) return;
- const appInstall = await prisma.appInstall.findUnique({
- where: { token },
- include: { device: { select: { uuid: true } } },
- });
- const uuid = appInstall?.device?.uuid;
- if (uuid) {
- leaveDeviceRoom(socket, uuid);
- // 移除 token 连接跟踪
- removeTokenConnection(token, socket.id);
- if (socket.data.tokens) socket.data.tokens.delete(token);
+ io.on("connection", (socket) => {
+ // 初始化每个连接所加入的设备房间集合
+ socket.data.deviceUuids = new Set();
+
+ // 仅允许通过 query.token/apptoken 加入
+ const qToken = socket.handshake?.query?.token || socket.handshake?.query?.apptoken;
+ if (qToken && typeof qToken === "string") {
+ joinByToken(socket, qToken).catch(() => {
+ });
}
- } catch {
- // ignore
- }
- });
- // 离开所有已加入的设备房间
- socket.on("leave-all", () => {
- const uuids = Array.from(socket.data.deviceUuids || []);
- uuids.forEach((u) => leaveDeviceRoom(socket, u));
- });
-
- // 聊天室:发送文本消息到加入的设备频道
- socket.on("chat:send", (data) => {
- try {
- const text = typeof data === "string" ? data : data?.text;
- if (typeof text !== "string") return;
- const trimmed = text.trim();
- if (!trimmed) return;
-
- // 限制消息最大长度,避免滥用
- const MAX_LEN = 2000;
- const safeText = trimmed.length > MAX_LEN ? trimmed.slice(0, MAX_LEN) : trimmed;
-
- const uuids = Array.from(socket.data.deviceUuids || []);
- if (uuids.length === 0) return;
-
- const at = new Date().toISOString();
- const payload = { text: safeText, at, senderId: socket.id };
-
- uuids.forEach((uuid) => {
- io.to(uuid).emit("chat:message", { uuid, ...payload });
+ // 客户端使用 KV token 加入房间
+ socket.on("join-token", (payload) => {
+ const token = payload?.token || payload?.apptoken;
+ if (typeof token === "string" && token.length > 0) {
+ joinByToken(socket, token).catch(() => {
+ });
+ }
+ });
+
+ // 客户端使用 token 离开房间
+ socket.on("leave-token", async (payload) => {
+ try {
+ const token = payload?.token || payload?.apptoken;
+ if (typeof token !== "string" || token.length === 0) return;
+ const appInstall = await prisma.appInstall.findUnique({
+ where: {token},
+ include: {device: {select: {uuid: true}}},
+ });
+ const uuid = appInstall?.device?.uuid;
+ if (uuid) {
+ leaveDeviceRoom(socket, uuid);
+ // 移除 token 连接跟踪
+ removeTokenConnection(token, socket.id);
+ if (socket.data.tokens) socket.data.tokens.delete(token);
+ }
+ } catch {
+ // ignore
+ }
+ });
+
+ // 离开所有已加入的设备房间
+ socket.on("leave-all", () => {
+ const uuids = Array.from(socket.data.deviceUuids || []);
+ uuids.forEach((u) => leaveDeviceRoom(socket, u));
+ });
+
+ // 聊天室:发送文本消息到加入的设备频道
+ socket.on("chat:send", (data) => {
+ try {
+ const text = typeof data === "string" ? data : data?.text;
+ if (typeof text !== "string") return;
+ const trimmed = text.trim();
+ if (!trimmed) return;
+
+ // 限制消息最大长度,避免滥用
+ const MAX_LEN = 2000;
+ const safeText = trimmed.length > MAX_LEN ? trimmed.slice(0, MAX_LEN) : trimmed;
+
+ const uuids = Array.from(socket.data.deviceUuids || []);
+ if (uuids.length === 0) return;
+
+ const at = new Date().toISOString();
+ const payload = {text: safeText, at, senderId: socket.id};
+
+ uuids.forEach((uuid) => {
+ io.to(uuid).emit("chat:message", {uuid, ...payload});
+ });
+ } catch (err) {
+ console.error("chat:send error:", err);
+ }
+ });
+
+ socket.on("disconnect", () => {
+ const uuids = Array.from(socket.data.deviceUuids || []);
+ uuids.forEach((u) => removeOnline(u, socket.id));
+
+ // 清理 token 连接跟踪
+ const tokens = Array.from(socket.data.tokens || []);
+ tokens.forEach((token) => removeTokenConnection(token, socket.id));
});
- } catch (err) {
- console.error("chat:send error:", err);
- }
});
- socket.on("disconnect", () => {
- const uuids = Array.from(socket.data.deviceUuids || []);
- uuids.forEach((u) => removeOnline(u, socket.id));
-
- // 清理 token 连接跟踪
- const tokens = Array.from(socket.data.tokens || []);
- tokens.forEach((token) => removeTokenConnection(token, socket.id));
- });
- });
-
- return io;
+ return io;
}
/** 返回 Socket.IO 实例 */
export function getIO() {
- return io;
+ return io;
}
/**
@@ -134,15 +136,15 @@ export function getIO() {
* @param {string} uuid
*/
function joinDeviceRoom(socket, uuid) {
- socket.join(uuid);
- if (!socket.data.deviceUuids) socket.data.deviceUuids = new Set();
- socket.data.deviceUuids.add(uuid);
- // 记录在线
- const set = onlineMap.get(uuid) || new Set();
- set.add(socket.id);
- onlineMap.set(uuid, set);
- // 可选:通知加入
- io.to(uuid).emit("device-joined", { uuid, connections: set.size });
+ socket.join(uuid);
+ if (!socket.data.deviceUuids) socket.data.deviceUuids = new Set();
+ socket.data.deviceUuids.add(uuid);
+ // 记录在线
+ const set = onlineMap.get(uuid) || new Set();
+ set.add(socket.id);
+ onlineMap.set(uuid, set);
+ // 可选:通知加入
+ io.to(uuid).emit("device-joined", {uuid, connections: set.size});
}
/**
@@ -151,16 +153,16 @@ function joinDeviceRoom(socket, uuid) {
* @param {string} token
*/
function trackTokenConnection(socket, token) {
- if (!socket.data.tokens) socket.data.tokens = new Set();
- socket.data.tokens.add(token);
+ if (!socket.data.tokens) socket.data.tokens = new Set();
+ socket.data.tokens.add(token);
- // 记录 token 连接
- const set = onlineTokens.get(token) || new Set();
- set.add(socket.id);
- onlineTokens.set(token, set);
+ // 记录 token 连接
+ const set = onlineTokens.get(token) || new Set();
+ set.add(socket.id);
+ onlineTokens.set(token, set);
- // 更新在线设备数指标(基于不同的 token 数量)
- onlineDevicesGauge.set(onlineTokens.size);
+ // 更新在线设备数指标(基于不同的 token 数量)
+ onlineDevicesGauge.set(onlineTokens.size);
}
/**
@@ -169,20 +171,20 @@ function trackTokenConnection(socket, token) {
* @param {string} uuid
*/
function leaveDeviceRoom(socket, uuid) {
- socket.leave(uuid);
- if (socket.data.deviceUuids) socket.data.deviceUuids.delete(uuid);
- removeOnline(uuid, socket.id);
+ socket.leave(uuid);
+ if (socket.data.deviceUuids) socket.data.deviceUuids.delete(uuid);
+ removeOnline(uuid, socket.id);
}
function removeOnline(uuid, socketId) {
- const set = onlineMap.get(uuid);
- if (!set) return;
- set.delete(socketId);
- if (set.size === 0) {
- onlineMap.delete(uuid);
- } else {
- onlineMap.set(uuid, set);
- }
+ const set = onlineMap.get(uuid);
+ if (!set) return;
+ set.delete(socketId);
+ if (set.size === 0) {
+ onlineMap.delete(uuid);
+ } else {
+ onlineMap.set(uuid, set);
+ }
}
/**
@@ -191,16 +193,16 @@ function removeOnline(uuid, socketId) {
* @param {string} socketId
*/
function removeTokenConnection(token, socketId) {
- const set = onlineTokens.get(token);
- if (!set) return;
- set.delete(socketId);
- if (set.size === 0) {
- onlineTokens.delete(token);
- } else {
- onlineTokens.set(token, set);
- }
- // 更新在线设备数指标(基于不同的 token 数量)
- onlineDevicesGauge.set(onlineTokens.size);
+ const set = onlineTokens.get(token);
+ if (!set) return;
+ set.delete(socketId);
+ if (set.size === 0) {
+ onlineTokens.delete(token);
+ } else {
+ onlineTokens.set(token, set);
+ }
+ // 更新在线设备数指标(基于不同的 token 数量)
+ onlineDevicesGauge.set(onlineTokens.size);
}
/**
@@ -209,8 +211,8 @@ function removeTokenConnection(token, socketId) {
* @param {object} payload { key, action: 'upsert'|'delete'|'batch', updatedAt?, created? }
*/
export function broadcastKeyChanged(uuid, payload) {
- if (!io || !uuid) return;
- io.to(uuid).emit("kv-key-changed", { uuid, ...payload });
+ if (!io || !uuid) return;
+ io.to(uuid).emit("kv-key-changed", {uuid, ...payload});
}
/**
@@ -218,19 +220,19 @@ export function broadcastKeyChanged(uuid, payload) {
* @returns {Array<{token:string, connections:number}>}
*/
export function getOnlineDevices() {
- const list = [];
- for (const [token, set] of onlineTokens.entries()) {
- list.push({ token, connections: set.size });
- }
- // 默认按连接数降序
- return list.sort((a, b) => b.connections - a.connections);
+ const list = [];
+ for (const [token, set] of onlineTokens.entries()) {
+ list.push({token, connections: set.size});
+ }
+ // 默认按连接数降序
+ return list.sort((a, b) => b.connections - a.connections);
}
export default {
- initSocket,
- getIO,
- broadcastKeyChanged,
- getOnlineDevices,
+ initSocket,
+ getIO,
+ broadcastKeyChanged,
+ getOnlineDevices,
};
/**
@@ -239,18 +241,18 @@ export default {
* @param {string} token
*/
async function joinByToken(socket, token) {
- const appInstall = await prisma.appInstall.findUnique({
- where: { token },
- include: { device: { select: { uuid: true } } },
- });
- const uuid = appInstall?.device?.uuid;
- if (uuid) {
- joinDeviceRoom(socket, uuid);
- // 跟踪 token 连接用于指标统计
- trackTokenConnection(socket, token);
- // 可选:回执
- socket.emit("joined", { by: "token", uuid, token });
- } else {
- socket.emit("join-error", { by: "token", reason: "invalid_token" });
- }
+ const appInstall = await prisma.appInstall.findUnique({
+ where: {token},
+ include: {device: {select: {uuid: true}}},
+ });
+ const uuid = appInstall?.device?.uuid;
+ if (uuid) {
+ joinDeviceRoom(socket, uuid);
+ // 跟踪 token 连接用于指标统计
+ trackTokenConnection(socket, token);
+ // 可选:回执
+ socket.emit("joined", {by: "token", uuid, token});
+ } else {
+ socket.emit("join-error", {by: "token", reason: "invalid_token"});
+ }
}
diff --git a/utils/tokenManager.js b/utils/tokenManager.js
index 5d25495..fc1716f 100644
--- a/utils/tokenManager.js
+++ b/utils/tokenManager.js
@@ -1,6 +1,6 @@
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
-import { PrismaClient } from '@prisma/client';
+import {PrismaClient} from '@prisma/client';
const prisma = new PrismaClient();
@@ -25,266 +25,271 @@ const REFRESH_TOKEN_PUBLIC_KEY = process.env.REFRESH_TOKEN_PUBLIC_KEY?.replace(/
* 获取签名和验证密钥
*/
function getKeys(tokenType = 'access') {
- if (JWT_ALG === 'RS256') {
- const privateKey = tokenType === 'access' ? ACCESS_TOKEN_PRIVATE_KEY : REFRESH_TOKEN_PRIVATE_KEY;
- const publicKey = tokenType === 'access' ? ACCESS_TOKEN_PUBLIC_KEY : REFRESH_TOKEN_PUBLIC_KEY;
+ if (JWT_ALG === 'RS256') {
+ const privateKey = tokenType === 'access' ? ACCESS_TOKEN_PRIVATE_KEY : REFRESH_TOKEN_PRIVATE_KEY;
+ const publicKey = tokenType === 'access' ? ACCESS_TOKEN_PUBLIC_KEY : REFRESH_TOKEN_PUBLIC_KEY;
- if (!privateKey || !publicKey) {
- throw new Error(`RS256 需要同时提供 ${tokenType.toUpperCase()}_TOKEN_PRIVATE_KEY 与 ${tokenType.toUpperCase()}_TOKEN_PUBLIC_KEY`);
+ if (!privateKey || !publicKey) {
+ throw new Error(`RS256 需要同时提供 ${tokenType.toUpperCase()}_TOKEN_PRIVATE_KEY 与 ${tokenType.toUpperCase()}_TOKEN_PUBLIC_KEY`);
+ }
+ return {signKey: privateKey, verifyKey: publicKey};
}
- return { signKey: privateKey, verifyKey: publicKey };
- }
- // 默认 HS256
- const secret = tokenType === 'access' ? ACCESS_TOKEN_SECRET : REFRESH_TOKEN_SECRET;
- return { signKey: secret, verifyKey: secret };
+ // 默认 HS256
+ const secret = tokenType === 'access' ? ACCESS_TOKEN_SECRET : REFRESH_TOKEN_SECRET;
+ return {signKey: secret, verifyKey: secret};
}
/**
* 生成访问令牌
*/
export function generateAccessToken(account) {
- const { signKey } = getKeys('access');
+ const {signKey} = getKeys('access');
- const payload = {
- type: 'access',
- accountId: account.id,
- provider: account.provider,
- email: account.email,
- name: account.name,
- avatarUrl: account.avatarUrl,
- tokenVersion: account.tokenVersion || 1,
- };
+ const payload = {
+ type: 'access',
+ accountId: account.id,
+ provider: account.provider,
+ email: account.email,
+ name: account.name,
+ avatarUrl: account.avatarUrl,
+ tokenVersion: account.tokenVersion || 1,
+ };
- return jwt.sign(payload, signKey, {
- expiresIn: ACCESS_TOKEN_EXPIRES_IN,
- algorithm: JWT_ALG,
- issuer: 'ClassworksKV',
- audience: 'classworks-client',
- });
+ return jwt.sign(payload, signKey, {
+ expiresIn: ACCESS_TOKEN_EXPIRES_IN,
+ algorithm: JWT_ALG,
+ issuer: 'ClassworksKV',
+ audience: 'classworks-client',
+ });
}
/**
* 生成刷新令牌
*/
export function generateRefreshToken(account) {
- const { signKey } = getKeys('refresh');
+ const {signKey} = getKeys('refresh');
- const payload = {
- type: 'refresh',
- accountId: account.id,
- tokenVersion: account.tokenVersion || 1,
- // 添加随机字符串增加安全性
- jti: crypto.randomBytes(16).toString('hex'),
- };
+ const payload = {
+ type: 'refresh',
+ accountId: account.id,
+ tokenVersion: account.tokenVersion || 1,
+ // 添加随机字符串增加安全性
+ jti: crypto.randomBytes(16).toString('hex'),
+ };
- return jwt.sign(payload, signKey, {
- expiresIn: REFRESH_TOKEN_EXPIRES_IN,
- algorithm: JWT_ALG,
- issuer: 'ClassworksKV',
- audience: 'classworks-client',
- });
+ return jwt.sign(payload, signKey, {
+ expiresIn: REFRESH_TOKEN_EXPIRES_IN,
+ algorithm: JWT_ALG,
+ issuer: 'ClassworksKV',
+ audience: 'classworks-client',
+ });
}
/**
* 验证访问令牌
*/
export function verifyAccessToken(token) {
- const { verifyKey } = getKeys('access');
+ const {verifyKey} = getKeys('access');
- try {
- const decoded = jwt.verify(token, verifyKey, {
- algorithms: [JWT_ALG],
- issuer: 'ClassworksKV',
- audience: 'classworks-client',
- });
+ try {
+ const decoded = jwt.verify(token, verifyKey, {
+ algorithms: [JWT_ALG],
+ issuer: 'ClassworksKV',
+ audience: 'classworks-client',
+ });
- if (decoded.type !== 'access') {
- throw new Error('Invalid token type');
+ if (decoded.type !== 'access') {
+ throw new Error('Invalid token type');
+ }
+
+ return decoded;
+ } catch (error) {
+ throw error;
}
-
- return decoded;
- } catch (error) {
- throw error;
- }
}
/**
* 验证刷新令牌
*/
export function verifyRefreshToken(token) {
- const { verifyKey } = getKeys('refresh');
+ const {verifyKey} = getKeys('refresh');
- try {
- const decoded = jwt.verify(token, verifyKey, {
- algorithms: [JWT_ALG],
- issuer: 'ClassworksKV',
- audience: 'classworks-client',
- });
+ try {
+ const decoded = jwt.verify(token, verifyKey, {
+ algorithms: [JWT_ALG],
+ issuer: 'ClassworksKV',
+ audience: 'classworks-client',
+ });
- if (decoded.type !== 'refresh') {
- throw new Error('Invalid token type');
+ if (decoded.type !== 'refresh') {
+ throw new Error('Invalid token type');
+ }
+
+ return decoded;
+ } catch (error) {
+ throw error;
}
-
- return decoded;
- } catch (error) {
- throw error;
- }
}
/**
* 生成令牌对(访问令牌 + 刷新令牌)
*/
export async function generateTokenPair(account) {
- const accessToken = generateAccessToken(account);
- const refreshToken = generateRefreshToken(account);
+ const accessToken = generateAccessToken(account);
+ const refreshToken = generateRefreshToken(account);
- // 计算刷新令牌过期时间
- const refreshTokenExpiry = new Date();
- const expiresInMs = parseExpirationToMs(REFRESH_TOKEN_EXPIRES_IN);
- refreshTokenExpiry.setTime(refreshTokenExpiry.getTime() + expiresInMs);
+ // 计算刷新令牌过期时间
+ const refreshTokenExpiry = new Date();
+ const expiresInMs = parseExpirationToMs(REFRESH_TOKEN_EXPIRES_IN);
+ refreshTokenExpiry.setTime(refreshTokenExpiry.getTime() + expiresInMs);
- // 更新数据库中的刷新令牌
- await prisma.account.update({
- where: { id: account.id },
- data: {
- refreshToken,
- refreshTokenExpiry,
- updatedAt: new Date(),
- },
- });
+ // 更新数据库中的刷新令牌
+ await prisma.account.update({
+ where: {id: account.id},
+ data: {
+ refreshToken,
+ refreshTokenExpiry,
+ updatedAt: new Date(),
+ },
+ });
- return {
- accessToken,
- refreshToken,
- accessTokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
- refreshTokenExpiresIn: REFRESH_TOKEN_EXPIRES_IN,
- };
+ return {
+ accessToken,
+ refreshToken,
+ accessTokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
+ refreshTokenExpiresIn: REFRESH_TOKEN_EXPIRES_IN,
+ };
}
/**
* 刷新访问令牌
*/
export async function refreshAccessToken(refreshToken) {
- try {
- // 验证刷新令牌
- const decoded = verifyRefreshToken(refreshToken);
+ try {
+ // 验证刷新令牌
+ const decoded = verifyRefreshToken(refreshToken);
- // 从数据库获取账户信息
- const account = await prisma.account.findUnique({
- where: { id: decoded.accountId },
- });
+ // 从数据库获取账户信息
+ const account = await prisma.account.findUnique({
+ where: {id: decoded.accountId},
+ });
- if (!account) {
- throw new Error('Account not found');
+ if (!account) {
+ throw new Error('Account not found');
+ }
+
+ // 验证刷新令牌是否匹配
+ if (account.refreshToken !== refreshToken) {
+ throw new Error('Invalid refresh token');
+ }
+
+ // 验证刷新令牌是否过期
+ if (account.refreshTokenExpiry && account.refreshTokenExpiry < new Date()) {
+ throw new Error('Refresh token expired');
+ }
+
+ // 验证令牌版本
+ if (account.tokenVersion !== decoded.tokenVersion) {
+ throw new Error('Token version mismatch');
+ }
+
+ // 生成新的访问令牌
+ const newAccessToken = generateAccessToken(account);
+
+ return {
+ accessToken: newAccessToken,
+ accessTokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
+ account: {
+ id: account.id,
+ provider: account.provider,
+ email: account.email,
+ name: account.name,
+ avatarUrl: account.avatarUrl,
+ },
+ };
+ } catch (error) {
+ throw error;
}
-
- // 验证刷新令牌是否匹配
- if (account.refreshToken !== refreshToken) {
- throw new Error('Invalid refresh token');
- }
-
- // 验证刷新令牌是否过期
- if (account.refreshTokenExpiry && account.refreshTokenExpiry < new Date()) {
- throw new Error('Refresh token expired');
- }
-
- // 验证令牌版本
- if (account.tokenVersion !== decoded.tokenVersion) {
- throw new Error('Token version mismatch');
- }
-
- // 生成新的访问令牌
- const newAccessToken = generateAccessToken(account);
-
- return {
- accessToken: newAccessToken,
- accessTokenExpiresIn: ACCESS_TOKEN_EXPIRES_IN,
- account: {
- id: account.id,
- provider: account.provider,
- email: account.email,
- name: account.name,
- avatarUrl: account.avatarUrl,
- },
- };
- } catch (error) {
- throw error;
- }
}
/**
* 撤销所有令牌(登出所有设备)
*/
export async function revokeAllTokens(accountId) {
- await prisma.account.update({
- where: { id: accountId },
- data: {
- tokenVersion: { increment: 1 },
- refreshToken: null,
- refreshTokenExpiry: null,
- updatedAt: new Date(),
- },
- });
+ await prisma.account.update({
+ where: {id: accountId},
+ data: {
+ tokenVersion: {increment: 1},
+ refreshToken: null,
+ refreshTokenExpiry: null,
+ updatedAt: new Date(),
+ },
+ });
}
/**
* 撤销当前刷新令牌(登出当前设备)
*/
export async function revokeRefreshToken(accountId) {
- await prisma.account.update({
- where: { id: accountId },
- data: {
- refreshToken: null,
- refreshTokenExpiry: null,
- updatedAt: new Date(),
- },
- });
+ await prisma.account.update({
+ where: {id: accountId},
+ data: {
+ refreshToken: null,
+ refreshTokenExpiry: null,
+ updatedAt: new Date(),
+ },
+ });
}
/**
* 解析过期时间字符串为毫秒
*/
function parseExpirationToMs(expiresIn) {
- if (typeof expiresIn === 'number') {
- return expiresIn * 1000;
- }
+ if (typeof expiresIn === 'number') {
+ return expiresIn * 1000;
+ }
- const match = expiresIn.match(/^(\d+)([smhd])$/);
- if (!match) {
- throw new Error('Invalid expiration format');
- }
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
+ if (!match) {
+ throw new Error('Invalid expiration format');
+ }
- const value = parseInt(match[1]);
- const unit = match[2];
+ const value = parseInt(match[1]);
+ const unit = match[2];
- switch (unit) {
- case 's': return value * 1000;
- case 'm': return value * 60 * 1000;
- case 'h': return value * 60 * 60 * 1000;
- case 'd': return value * 24 * 60 * 60 * 1000;
- default: throw new Error('Invalid time unit');
- }
+ switch (unit) {
+ case 's':
+ return value * 1000;
+ case 'm':
+ return value * 60 * 1000;
+ case 'h':
+ return value * 60 * 60 * 1000;
+ case 'd':
+ return value * 24 * 60 * 60 * 1000;
+ default:
+ throw new Error('Invalid time unit');
+ }
}
/**
* 验证账户并检查令牌版本
*/
export async function validateAccountToken(decoded) {
- const account = await prisma.account.findUnique({
- where: { id: decoded.accountId },
- });
+ const account = await prisma.account.findUnique({
+ where: {id: decoded.accountId},
+ });
- if (!account) {
- throw new Error('Account not found');
- }
+ if (!account) {
+ throw new Error('Account not found');
+ }
- // 验证令牌版本
- if (account.tokenVersion !== decoded.tokenVersion) {
- throw new Error('Token version mismatch');
- }
+ // 验证令牌版本
+ if (account.tokenVersion !== decoded.tokenVersion) {
+ throw new Error('Token version mismatch');
+ }
- return account;
+ return account;
}
// 向后兼容的导出
diff --git a/views/index.ejs b/views/index.ejs
index 4b18bd7..379cf1b 100644
--- a/views/index.ejs
+++ b/views/index.ejs
@@ -2,14 +2,16 @@
-
-
- Classworks 服务端
+
+
+ Classworks 服务端
- Classworks 服务端
- 服务运行中
+Classworks 服务端
+服务运行中
-
+
\ No newline at end of file