mirror of
https://github.com/ZeroCatDev/ClassworksKV.git
synced 2025-07-01 20:09:23 +00:00
483 lines
12 KiB
JavaScript
483 lines
12 KiB
JavaScript
import { Router } from "express";
|
||
const router = Router();
|
||
import kvStore from "../models/kvStore.js";
|
||
import { checkSiteKey } from "../middleware/auth.js";
|
||
import { v4 as uuidv4 } from "uuid";
|
||
import errors from "../utils/errors.js";
|
||
import { PrismaClient } from "@prisma/client";
|
||
import {
|
||
readAuthMiddleware,
|
||
writeAuthMiddleware,
|
||
removePasswordMiddleware,
|
||
deviceInfoMiddleware,
|
||
} from "../middleware/auth.js";
|
||
import { hashPassword, verifyPassword } from "../utils/crypto.js";
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
// 定义有效的访问类型
|
||
const VALID_ACCESS_TYPES = ["PUBLIC", "PROTECTED", "PRIVATE"];
|
||
|
||
// 检查是否为受限UUID的中间件
|
||
const checkRestrictedUUID = (req, res, next) => {
|
||
const restrictedUUID = "00000000-0000-4000-8000-000000000000";
|
||
const namespace = req.params.namespace;
|
||
|
||
if (namespace === restrictedUUID) {
|
||
return next(errors.createError(403, "无权限访问此命名空间"));
|
||
}
|
||
next();
|
||
};
|
||
|
||
router.use(checkSiteKey);
|
||
|
||
// Get device info
|
||
router.get(
|
||
"/:namespace/_info",
|
||
checkRestrictedUUID,
|
||
readAuthMiddleware,
|
||
errors.catchAsync(async (req, res) => {
|
||
const device = res.locals.device;
|
||
if (!device) {
|
||
return res.status(404).json({
|
||
statusCode: 404,
|
||
message: "设备不存在",
|
||
});
|
||
}
|
||
res.json({
|
||
uuid: device.uuid,
|
||
name: device.name,
|
||
accessType: device.accessType,
|
||
hasPassword: !!device.password,
|
||
});
|
||
})
|
||
);
|
||
|
||
// Get device info
|
||
router.get(
|
||
"/:namespace/_check",
|
||
checkRestrictedUUID,
|
||
deviceInfoMiddleware,
|
||
errors.catchAsync(async (req, res) => {
|
||
const device = res.locals.device;
|
||
if (!device) {
|
||
return res.status(404).json({
|
||
statusCode: 404,
|
||
message: "设备不存在",
|
||
});
|
||
}
|
||
res.json({
|
||
status: "success",
|
||
uuid: device.uuid,
|
||
name: device.name,
|
||
accessType: device.accessType,
|
||
hasPassword: !!device.password,
|
||
});
|
||
})
|
||
);
|
||
|
||
// Get device info
|
||
router.post(
|
||
"/:namespace/_checkpassword",
|
||
checkRestrictedUUID,
|
||
deviceInfoMiddleware,
|
||
errors.catchAsync(async (req, res) => {
|
||
const { password } = req.body;
|
||
const device = res.locals.device;
|
||
if (!device) {
|
||
return res.status(404).json({
|
||
statusCode: 404,
|
||
message: "设备不存在",
|
||
});
|
||
}
|
||
const isPasswordValid = await verifyPassword(password, device.password);
|
||
if (!isPasswordValid) {
|
||
return res.status(401).json({
|
||
statusCode: 401,
|
||
message: "密码错误",
|
||
});
|
||
}
|
||
res.json({
|
||
status: "success",
|
||
uuid: device.uuid,
|
||
name: device.name,
|
||
accessType: device.accessType,
|
||
hasPassword: !!device.password,
|
||
});
|
||
})
|
||
);
|
||
// Get password hint
|
||
router.get(
|
||
"/:namespace/_hint",
|
||
checkRestrictedUUID,
|
||
errors.catchAsync(async (req, res) => {
|
||
const { namespace } = req.params;
|
||
|
||
const device = await prisma.device.findUnique({
|
||
where: { uuid: namespace },
|
||
select: { passwordHint: true },
|
||
});
|
||
|
||
if (!device) {
|
||
return res.status(404).json({
|
||
statusCode: 404,
|
||
message: "设备不存在",
|
||
});
|
||
}
|
||
|
||
res.json({
|
||
passwordHint: device.passwordHint || null,
|
||
});
|
||
})
|
||
);
|
||
|
||
// Update password hint
|
||
router.put(
|
||
"/:namespace/_hint",
|
||
checkRestrictedUUID,
|
||
writeAuthMiddleware,
|
||
errors.catchAsync(async (req, res) => {
|
||
const { hint } = req.body;
|
||
const device = res.locals.device;
|
||
|
||
const updatedDevice = await prisma.device.update({
|
||
where: { uuid: device.uuid },
|
||
data: { passwordHint: hint },
|
||
select: { passwordHint: true },
|
||
});
|
||
|
||
res.json({
|
||
message: "密码提示已更新",
|
||
passwordHint: updatedDevice.passwordHint,
|
||
});
|
||
})
|
||
);
|
||
|
||
// Update device password
|
||
router.post(
|
||
"/:namespace/_password",
|
||
writeAuthMiddleware,
|
||
errors.catchAsync(async (req, res, next) => {
|
||
const { password, oldPassword } = req.body;
|
||
const device = res.locals.device;
|
||
|
||
try {
|
||
// 验证旧密码
|
||
if (
|
||
device.password &&
|
||
!(await verifyPassword(oldPassword, device.password))
|
||
) {
|
||
return next(errors.createError(500, "密码错误"));
|
||
}
|
||
|
||
// 对新密码进行哈希处理
|
||
const hashedPassword = await hashPassword(password);
|
||
if (!hashedPassword) {
|
||
return next(errors.createError(400, "新密码格式无效"));
|
||
}
|
||
|
||
await prisma.device.update({
|
||
where: { uuid: device.uuid },
|
||
data: {
|
||
password: hashedPassword,
|
||
accessType: VALID_ACCESS_TYPES[1], // 设置密码时默认为受保护模式
|
||
},
|
||
});
|
||
|
||
res.json({ message: "密码已成功修改" });
|
||
} catch (error) {
|
||
return next(errors.createError(500, "无法修改密码"));
|
||
}
|
||
})
|
||
);
|
||
|
||
// Update device info
|
||
router.put(
|
||
"/:namespace/_info",
|
||
writeAuthMiddleware,
|
||
errors.catchAsync(async (req, res) => {
|
||
const { name, accessType } = req.body;
|
||
const device = res.locals.device;
|
||
|
||
// 验证 accessType
|
||
if (accessType && !VALID_ACCESS_TYPES.includes(accessType)) {
|
||
return res.status(400).json({
|
||
error: `Invalid access type. Must be one of: ${VALID_ACCESS_TYPES.join(
|
||
", "
|
||
)}`,
|
||
});
|
||
}
|
||
|
||
const updatedDevice = await prisma.device.update({
|
||
where: { uuid: device.uuid },
|
||
data: {
|
||
name: name || device.name,
|
||
accessType: accessType || device.accessType,
|
||
},
|
||
});
|
||
|
||
res.json({
|
||
uuid: updatedDevice.uuid,
|
||
name: updatedDevice.name,
|
||
accessType: updatedDevice.accessType,
|
||
hasPassword: !!updatedDevice.password,
|
||
});
|
||
})
|
||
);
|
||
|
||
// Remove device password
|
||
router.delete(
|
||
"/:namespace/_password",
|
||
removePasswordMiddleware,
|
||
errors.catchAsync(async (req, res) => {
|
||
res.json({ message: "密码已成功移除" });
|
||
})
|
||
);
|
||
|
||
/**
|
||
* GET /:namespace
|
||
* 获取指定命名空间下的所有键名及元数据列表
|
||
*/
|
||
router.get(
|
||
"/:namespace",
|
||
checkRestrictedUUID,
|
||
errors.catchAsync(async (req, res, next) => {
|
||
const { namespace } = req.params;
|
||
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 keys = await kvStore.list(namespace, options);
|
||
|
||
// 获取总记录数
|
||
const totalRows = await kvStore.count(namespace);
|
||
|
||
// 构建响应对象
|
||
const response = {
|
||
items: keys,
|
||
total_rows: totalRows,
|
||
};
|
||
|
||
// 如果还有更多数据,添加load_more字段
|
||
const nextSkip = options.skip + options.limit;
|
||
if (nextSkip < totalRows) {
|
||
const baseUrl = `${req.baseUrl}/${namespace}`;
|
||
const queryParams = new URLSearchParams({
|
||
sortBy: options.sortBy,
|
||
sortDir: options.sortDir,
|
||
limit: options.limit,
|
||
skip: nextSkip,
|
||
}).toString();
|
||
|
||
response.load_more = `${baseUrl}?${queryParams}`;
|
||
}
|
||
|
||
return res.json(response);
|
||
})
|
||
);
|
||
|
||
/**
|
||
* GET /:namespace/:key
|
||
* 通过命名空间和键名获取键值
|
||
*/
|
||
router.get(
|
||
"/:namespace/:key",
|
||
checkRestrictedUUID,
|
||
readAuthMiddleware,
|
||
errors.catchAsync(async (req, res, next) => {
|
||
const { namespace, key } = req.params;
|
||
|
||
// 否则只返回值
|
||
const value = await kvStore.get(namespace, key);
|
||
|
||
if (value === null) {
|
||
// 创建并传递错误,而不是抛出
|
||
return next(
|
||
errors.createError(
|
||
404,
|
||
`未找到命名空间 '${namespace}' 下键名为 '${key}' 的记录`
|
||
)
|
||
);
|
||
}
|
||
|
||
return res.json(value);
|
||
})
|
||
);
|
||
router.get(
|
||
"/:namespace/:key/metadata",
|
||
checkRestrictedUUID,
|
||
readAuthMiddleware,
|
||
errors.catchAsync(async (req, res, next) => {
|
||
const { namespace, key } = req.params;
|
||
const metadata = await kvStore.getMetadata(namespace, key);
|
||
if (!metadata) {
|
||
return next(
|
||
errors.createError(
|
||
404,
|
||
`未找到命名空间 '${namespace}' 下键名为 '${key}' 的记录`
|
||
)
|
||
);
|
||
}
|
||
return res.json(metadata);
|
||
})
|
||
);
|
||
|
||
/**
|
||
* POST /:namespace/batch-import
|
||
* 批量导入键值对到指定命名空间
|
||
*/
|
||
router.post(
|
||
"/:namespace/_batchimport",
|
||
checkRestrictedUUID,
|
||
errors.catchAsync(async (req, res, next) => {
|
||
const { namespace } = req.params;
|
||
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 errors = [];
|
||
|
||
// 批量处理所有键值对
|
||
for (const [key, value] of Object.entries(data)) {
|
||
try {
|
||
const result = await kvStore.upsert(namespace, key, value, creatorIp);
|
||
results.push({
|
||
key: result.key,
|
||
created: result.createdAt.getTime() === result.updatedAt.getTime(),
|
||
});
|
||
} catch (error) {
|
||
errors.push({
|
||
key,
|
||
error: error.message,
|
||
});
|
||
}
|
||
}
|
||
|
||
return res.status(200).json({
|
||
namespace,
|
||
total: Object.keys(data).length,
|
||
successful: results.length,
|
||
failed: errors.length,
|
||
results,
|
||
errors: errors.length > 0 ? errors : undefined,
|
||
});
|
||
})
|
||
);
|
||
/**
|
||
* POST /:namespace/:key
|
||
* 更新指定命名空间下的键值,如果不存在则创建
|
||
*/
|
||
router.post(
|
||
"/:namespace/:key",
|
||
checkRestrictedUUID,
|
||
writeAuthMiddleware,
|
||
errors.catchAsync(async (req, res, next) => {
|
||
const { namespace, key } = req.params;
|
||
const value = req.body;
|
||
|
||
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 ||
|
||
"";
|
||
|
||
const result = await kvStore.upsert(namespace, key, value, creatorIp);
|
||
return res.status(200).json({
|
||
namespace: result.namespace,
|
||
key: result.key,
|
||
created: result.createdAt.getTime() === result.updatedAt.getTime(),
|
||
updatedAt: result.updatedAt,
|
||
});
|
||
})
|
||
);
|
||
|
||
/**
|
||
* DELETE /:namespace
|
||
* 删除指定命名空间及其所有键值对
|
||
*/
|
||
router.delete(
|
||
"/:namespace",
|
||
checkRestrictedUUID,
|
||
errors.catchAsync(async (req, res, next) => {
|
||
const { namespace } = req.params;
|
||
const result = await kvStore.deleteNamespace(namespace);
|
||
|
||
if (!result) {
|
||
// 创建并传递错误,而不是抛出
|
||
return next(errors.createError(404, `未找到命名空间 '${namespace}'`));
|
||
}
|
||
|
||
// 204状态码表示成功但无内容返回
|
||
return res.status(204).end();
|
||
})
|
||
);
|
||
|
||
/**
|
||
* DELETE /:namespace/:key
|
||
* 删除指定命名空间下的键值对
|
||
*/
|
||
router.delete(
|
||
"/:namespace/:key",
|
||
checkRestrictedUUID,
|
||
errors.catchAsync(async (req, res, next) => {
|
||
const { namespace, key } = req.params;
|
||
const result = await kvStore.delete(namespace, key);
|
||
|
||
if (!result) {
|
||
// 创建并传递错误,而不是抛出
|
||
return next(
|
||
errors.createError(
|
||
404,
|
||
`未找到命名空间 '${namespace}' 下键名为 '${key}' 的记录`
|
||
)
|
||
);
|
||
}
|
||
|
||
// 204状态码表示成功但无内容返回
|
||
return res.status(204).end();
|
||
})
|
||
);
|
||
|
||
/**
|
||
* GET /uuid
|
||
* 生成并返回一个随机UUID,可用作新命名空间
|
||
*/
|
||
router.get(
|
||
"/uuid",
|
||
errors.catchAsync(async (req, res) => {
|
||
const namespace = uuidv4();
|
||
res.json({ namespace });
|
||
})
|
||
);
|
||
|
||
export default router;
|