1
1
mirror of https://github.com/ZeroCatDev/ClassworksKV.git synced 2025-12-10 08:03:09 +00:00

Compare commits

..

No commits in common. "e65f84aa22b5960391c5649d57c8ff41564ac106" and "63716e04290a63ab170c6ba47cf944743b94f1f3" have entirely different histories.

3 changed files with 41 additions and 59 deletions

4
app.js
View File

@ -25,10 +25,6 @@ app.use(
cors({ cors({
exposedHeaders: ["ratelimit-policy", "retry-after", "ratelimit"], // 告诉浏览器这些响应头可以暴露 exposedHeaders: ["ratelimit-policy", "retry-after", "ratelimit"], // 告诉浏览器这些响应头可以暴露
maxAge: 86400, // 设置OPTIONS请求的结果缓存24小时(86400秒),减少预检请求 maxAge: 86400, // 设置OPTIONS请求的结果缓存24小时(86400秒),减少预检请求
credentials: true, // 允许跨域请求携带凭证
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept"], // 允许的请求头
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], // 允许的HTTP方法
withCredentials: true, // 允许携带cookie等凭证信息
}) })
); );
app.disable("x-powered-by"); app.disable("x-powered-by");

View File

@ -1,7 +1,7 @@
import { Router } from "express"; import {Router} from "express";
import kvStore from "../utils/kvStore.js"; import kvStore from "../utils/kvStore.js";
import { broadcastKeyChanged } from "../utils/socket.js"; import {broadcastKeyChanged} from "../utils/socket.js";
import { kvTokenAuth } from "../middleware/kvTokenAuth.js"; import {kvTokenAuth} from "../middleware/kvTokenAuth.js";
import { import {
prepareTokenForRateLimit, prepareTokenForRateLimit,
tokenBatchLimiter, tokenBatchLimiter,
@ -10,7 +10,7 @@ import {
tokenWriteLimiter tokenWriteLimiter
} from "../middleware/rateLimiter.js"; } from "../middleware/rateLimiter.js";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";
import { PrismaClient } from "@prisma/client"; import {PrismaClient} from "@prisma/client";
const router = Router(); const router = Router();
@ -34,7 +34,7 @@ router.get(
// 获取设备信息,包含关联的账号 // 获取设备信息,包含关联的账号
const device = await prisma.device.findUnique({ const device = await prisma.device.findUnique({
where: { id: deviceId }, where: {id: deviceId},
include: { include: {
account: true, account: true,
}, },
@ -88,7 +88,7 @@ router.get(
// 查找当前 token 对应的应用安装记录 // 查找当前 token 对应的应用安装记录
const appInstall = await prisma.appInstall.findUnique({ const appInstall = await prisma.appInstall.findUnique({
where: { token }, where: {token},
include: { include: {
device: { device: {
select: { select: {
@ -133,7 +133,7 @@ router.get(
tokenReadLimiter, tokenReadLimiter,
errors.catchAsync(async (req, res) => { errors.catchAsync(async (req, res) => {
const deviceId = res.locals.deviceId; const deviceId = res.locals.deviceId;
const { sortBy, sortDir, limit, skip } = req.query; const {sortBy, sortDir, limit, skip} = req.query;
// 构建选项 // 构建选项
const options = { const options = {
@ -184,7 +184,7 @@ router.get(
tokenReadLimiter, tokenReadLimiter,
errors.catchAsync(async (req, res) => { errors.catchAsync(async (req, res) => {
const deviceId = res.locals.deviceId; const deviceId = res.locals.deviceId;
const { sortBy, sortDir, limit, skip } = req.query; const {sortBy, sortDir, limit, skip} = req.query;
// 构建选项 // 构建选项
const options = { const options = {
@ -230,7 +230,7 @@ router.get(
tokenReadLimiter, tokenReadLimiter,
errors.catchAsync(async (req, res, next) => { errors.catchAsync(async (req, res, next) => {
const deviceId = res.locals.deviceId; const deviceId = res.locals.deviceId;
const { key } = req.params; const {key} = req.params;
const value = await kvStore.get(deviceId, key); const value = await kvStore.get(deviceId, key);
@ -253,7 +253,7 @@ router.get(
tokenReadLimiter, tokenReadLimiter,
errors.catchAsync(async (req, res, next) => { errors.catchAsync(async (req, res, next) => {
const deviceId = res.locals.deviceId; const deviceId = res.locals.deviceId;
const { key } = req.params; const {key} = req.params;
const metadata = await kvStore.getMetadata(deviceId, key); const metadata = await kvStore.getMetadata(deviceId, key);
if (!metadata) { if (!metadata) {
@ -353,22 +353,10 @@ router.post(
} }
const deviceId = res.locals.deviceId; const deviceId = res.locals.deviceId;
const { key } = req.params; const {key} = req.params;
let value = req.body; const value = req.body;
// 处理空值,转换为空对象
if (value === null || value === undefined || value === '') {
value = {};
}
// 验证是否能被 JSON 序列化
try {
JSON.stringify(value);
} catch (error) {
return next(
errors.createError(400, "无效的数据格式")
);
}
// 获取客户端IP // 获取客户端IP
const creatorIp = const creatorIp =
@ -414,7 +402,7 @@ router.delete(
} }
const deviceId = res.locals.deviceId; const deviceId = res.locals.deviceId;
const { key } = req.params; const {key} = req.params;
const result = await kvStore.delete(deviceId, key); const result = await kvStore.delete(deviceId, key);

View File

@ -12,9 +12,9 @@
* - 令牌信息缓存在连接时预加载令牌详细信息以提高性能 * - 令牌信息缓存在连接时预加载令牌详细信息以提高性能
*/ */
import { Server } from "socket.io"; import {Server} from "socket.io";
import { PrismaClient } from "@prisma/client"; import {PrismaClient} from "@prisma/client";
import { onlineDevicesGauge } from "./metrics.js"; import {onlineDevicesGauge} from "./metrics.js";
import DeviceDetector from "node-device-detector"; import DeviceDetector from "node-device-detector";
import ClientHints from "node-device-detector/client-hints.js"; import ClientHints from "node-device-detector/client-hints.js";
@ -96,16 +96,14 @@ function detectDeviceName(userAgent, headers = {}) {
export function initSocket(server) { export function initSocket(server) {
if (io) return io; if (io) return io;
const allowOrigin = process.env.FRONTEND_URL || "*";
io = new Server(server, { io = new Server(server, {
cors: { cors: {
origin: "*", origin: allowOrigin,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', methods: ["GET", "POST"],
allowedHeaders: ["*"], credentials: true,
credentials: false
}, },
// 传输方式回退策略优先使用WebSocket,回退到轮询
transports: ["polling", "websocket"],
}); });
io.on("connection", (socket) => { io.on("connection", (socket) => {
@ -134,8 +132,8 @@ export function initSocket(server) {
const token = payload?.token || payload?.apptoken; const token = payload?.token || payload?.apptoken;
if (typeof token !== "string" || token.length === 0) return; if (typeof token !== "string" || token.length === 0) return;
const appInstall = await prisma.appInstall.findUnique({ const appInstall = await prisma.appInstall.findUnique({
where: { token }, where: {token},
include: { device: { select: { uuid: true } } }, include: {device: {select: {uuid: true}}},
}); });
const uuid = appInstall?.device?.uuid; const uuid = appInstall?.device?.uuid;
if (uuid) { if (uuid) {
@ -158,11 +156,11 @@ export function initSocket(server) {
// 获取事件历史记录 // 获取事件历史记录
socket.on("get-event-history", (data) => { socket.on("get-event-history", (data) => {
try { try {
const { limit = 50, offset = 0 } = data || {}; const {limit = 50, offset = 0} = data || {};
const uuids = Array.from(socket.data.deviceUuids || []); const uuids = Array.from(socket.data.deviceUuids || []);
if (uuids.length === 0) { if (uuids.length === 0) {
socket.emit("event-history-error", { reason: "not_joined_any_device" }); socket.emit("event-history-error", {reason: "not_joined_any_device"});
return; return;
} }
@ -184,7 +182,7 @@ export function initSocket(server) {
} catch (err) { } catch (err) {
console.error("get-event-history error:", err); console.error("get-event-history error:", err);
socket.emit("event-history-error", { reason: "internal_error" }); socket.emit("event-history-error", {reason: "internal_error"});
} }
}); });
@ -193,35 +191,35 @@ export function initSocket(server) {
try { try {
// 验证数据结构 // 验证数据结构
if (!data || typeof data !== "object") { if (!data || typeof data !== "object") {
socket.emit("event-error", { reason: "invalid_data_format" }); socket.emit("event-error", {reason: "invalid_data_format"});
return; return;
} }
const { type, content } = data; const {type, content} = data;
// 验证事件类型 // 验证事件类型
if (typeof type !== "string" || type.trim().length === 0) { if (typeof type !== "string" || type.trim().length === 0) {
socket.emit("event-error", { reason: "invalid_event_type" }); socket.emit("event-error", {reason: "invalid_event_type"});
return; return;
} }
// 验证内容格式必须是对象或null // 验证内容格式必须是对象或null
if (content !== null && (typeof content !== "object" || Array.isArray(content))) { if (content !== null && (typeof content !== "object" || Array.isArray(content))) {
socket.emit("event-error", { reason: "content_must_be_object_or_null" }); socket.emit("event-error", {reason: "content_must_be_object_or_null"});
return; return;
} }
// 获取当前socket所在的设备房间 // 获取当前socket所在的设备房间
const uuids = Array.from(socket.data.deviceUuids || []); const uuids = Array.from(socket.data.deviceUuids || []);
if (uuids.length === 0) { if (uuids.length === 0) {
socket.emit("event-error", { reason: "not_joined_any_device" }); socket.emit("event-error", {reason: "not_joined_any_device"});
return; return;
} }
// 检查只读权限 // 检查只读权限
const tokenInfo = socket.data.tokenInfo; const tokenInfo = socket.data.tokenInfo;
if (tokenInfo?.isReadOnly) { if (tokenInfo?.isReadOnly) {
socket.emit("event-error", { reason: "readonly_token_cannot_send_events" }); socket.emit("event-error", {reason: "readonly_token_cannot_send_events"});
return; return;
} }
@ -229,7 +227,7 @@ export function initSocket(server) {
const MAX_SIZE = 10240; // 10KB const MAX_SIZE = 10240; // 10KB
const serializedContent = JSON.stringify(content); const serializedContent = JSON.stringify(content);
if (serializedContent.length > MAX_SIZE) { if (serializedContent.length > MAX_SIZE) {
socket.emit("event-error", { reason: "content_too_large", maxSize: MAX_SIZE }); socket.emit("event-error", {reason: "content_too_large", maxSize: MAX_SIZE});
return; return;
} }
@ -276,7 +274,7 @@ export function initSocket(server) {
} catch (err) { } catch (err) {
console.error("send-event error:", err); console.error("send-event error:", err);
socket.emit("event-error", { reason: "internal_error" }); socket.emit("event-error", {reason: "internal_error"});
} }
}); });
@ -290,10 +288,10 @@ export function initSocket(server) {
// 清理socket相关缓存 // 清理socket相关缓存
if (socket.data.currentToken) { if (socket.data.currentToken) {
// 如果这是该token的最后一个连接,考虑清理缓存 // 如果这是该token的最后一个连接考虑清理缓存
const tokenSet = onlineTokens.get(socket.data.currentToken); const tokenSet = onlineTokens.get(socket.data.currentToken);
if (!tokenSet || tokenSet.size === 0) { if (!tokenSet || tokenSet.size === 0) {
// 可以选择保留缓存一段时间,这里暂时保留 // 可以选择保留缓存一段时间这里暂时保留
// tokenInfoCache.delete(socket.data.currentToken); // tokenInfoCache.delete(socket.data.currentToken);
} }
} }
@ -322,7 +320,7 @@ function joinDeviceRoom(socket, uuid) {
set.add(socket.id); set.add(socket.id);
onlineMap.set(uuid, set); onlineMap.set(uuid, set);
// 可选:通知加入 // 可选:通知加入
io.to(uuid).emit("device-joined", { uuid, connections: set.size }); io.to(uuid).emit("device-joined", {uuid, connections: set.size});
} }
/** /**
@ -527,7 +525,7 @@ function cleanupDeviceCache(uuid) {
export function getOnlineDevices() { export function getOnlineDevices() {
const list = []; const list = [];
for (const [token, set] of onlineTokens.entries()) { for (const [token, set] of onlineTokens.entries()) {
list.push({ token, connections: set.size }); list.push({token, connections: set.size});
} }
// 默认按连接数降序 // 默认按连接数降序
return list.sort((a, b) => b.connections - a.connections); return list.sort((a, b) => b.connections - a.connections);
@ -551,7 +549,7 @@ export default {
async function joinByToken(socket, token) { async function joinByToken(socket, token) {
try { try {
const appInstall = await prisma.appInstall.findUnique({ const appInstall = await prisma.appInstall.findUnique({
where: { token }, where: {token},
include: { include: {
device: { device: {
select: { select: {
@ -608,10 +606,10 @@ async function joinByToken(socket, token) {
} }
}); });
} else { } else {
socket.emit("join-error", { by: "token", reason: "invalid_token" }); socket.emit("join-error", {by: "token", reason: "invalid_token"});
} }
} catch (error) { } catch (error) {
console.error("joinByToken error:", error); console.error("joinByToken error:", error);
socket.emit("join-error", { by: "token", reason: "database_error" }); socket.emit("join-error", {by: "token", reason: "database_error"});
} }
} }