1
1
mirror of https://github.com/ZeroCatDev/ClassworksKV.git synced 2025-10-22 10:23:12 +00:00
2025-10-02 12:07:50 +08:00

394 lines
8.9 KiB
JavaScript

import { Router } from "express";
const router = Router();
import {
deviceMiddleware,
passwordMiddleware,
deviceInfoMiddleware,
} from "../middleware/device.js";
import { checkSiteKey } from "../middleware/auth.js";
import { PrismaClient } from "@prisma/client";
import crypto from "crypto";
import errors from "../utils/errors.js";
import { hashPassword, verifyDevicePassword } from "../utils/crypto.js";
const prisma = new PrismaClient();
router.use(checkSiteKey);
/**
* GET /apps
* 获取应用列表
*/
router.get(
"/",
errors.catchAsync(async (req, res) => {
const { limit = 20, skip = 0, search } = req.query;
const where = search
? {
OR: [
{ name: { contains: search } },
{ description: { contains: search } },
{ developerName: { contains: search } },
],
}
: {};
const [apps, total] = await Promise.all([
prisma.app.findMany({
where,
take: parseInt(limit),
skip: parseInt(skip),
orderBy: { createdAt: "desc" },
}),
prisma.app.count({ where }),
]);
res.json({
apps,
total,
limit: parseInt(limit),
skip: parseInt(skip),
});
})
);
/**
* GET /apps/:id
* 获取单个应用详情
*/
router.get(
"/:id",
errors.catchAsync(async (req, res) => {
const { id } = req.params;
const app = await prisma.app.findUnique({
where: { id: parseInt(id) },
});
if (!app) {
return res.status(404).json({
statusCode: 404,
message: "应用不存在",
});
}
res.json(app);
})
);
/**
* POST /apps/:id/authorize
* 为应用授权获取token
*
* 使用统一的设备中间件:
* 1. deviceMiddleware - 自动获取或创建设备
* 2. passwordMiddleware - 验证密码(如果设备有密码)
*
* 请求体:
* {
* "deviceUuid": "设备UUID",
* "password": "设备密码(如果设备有密码则必须提供)",
* "note": "备注信息" // 可选
* }
*/
router.post(
"/:id/authorize",
deviceMiddleware,
passwordMiddleware,
errors.catchAsync(async (req, res) => {
const { id: appId } = req.params;
const { note } = req.body;
const device = res.locals.device;
// 检查应用是否存在
const app = await prisma.app.findUnique({
where: { id: Number(appId) },
});
if (!app) {
return res.status(404).json({
statusCode: 404,
message: "应用不存在",
});
}
// 生成token
const randomBytes = crypto.randomBytes(32);
const tokenData = `${appId}-${device.uuid}-${Date.now()}-${randomBytes.toString('hex')}`;
const token = crypto.createHash("sha256").update(tokenData).digest("hex");
// 创建应用安装记录
const appInstall = await prisma.appInstall.create({
data: {
deviceId: device.id,
appId: Number(appId),
token,
note: note || "授权访问",
},
});
res.status(200).json({
token: appInstall.token,
appId: Number(appId),
appName: app.name,
deviceUuid: device.uuid,
deviceName: device.name,
note: appInstall.note,
authorizedAt: appInstall.installedAt,
});
})
);
/**
* GET /apps/devices/:deviceUuid/tokens
* 获取设备上的所有授权token
*/
router.get(
"/devices/:deviceUuid/tokens",
deviceInfoMiddleware,
errors.catchAsync(async (req, res) => {
const device = res.locals.device;
const installations = await prisma.appInstall.findMany({
where: { deviceId: device.id },
include: {
app: {
select: {
id: true,
name: true,
description: true,
developerName: true,
iconHash: true,
repositoryUrl: true,
createdAt: true,
updatedAt: true,
},
},
},
orderBy: { installedAt: "desc" },
});
res.json({
deviceUuid: device.uuid,
deviceName: device.name,
tokens: installations.map(install => ({
id: install.id,
token: install.token,
app: install.app,
note: install.note,
installedAt: install.installedAt,
updatedAt: install.updatedAt,
createdAt: install.createdAt,
repositoryUrl: install.app.repositoryUrl,
})),
total: installations.length,
});
})
);
/**
* DELETE /apps/tokens/:token
* 撤销特定token
*/
router.delete(
"/tokens/:token",
errors.catchAsync(async (req, res) => {
const { token } = req.params;
const result = await prisma.appInstall.deleteMany({
where: { token },
});
if (result.count === 0) {
return res.status(404).json({
statusCode: 404,
message: "Token不存在",
});
}
res.status(204).end();
})
);
/**
* GET /apps/:id/installations
* 获取应用的所有安装记录
*/
router.get(
"/:id/installations",
errors.catchAsync(async (req, res) => {
const { id: appId } = req.params;
const { limit = 20, skip = 0 } = req.query;
const [installations, total] = await Promise.all([
prisma.appInstall.findMany({
where: { appId: Number(appId) },
include: {
device: {
select: {
uuid: true,
name: true,
},
},
},
take: parseInt(limit),
skip: parseInt(skip),
orderBy: { installedAt: "desc" },
}),
prisma.appInstall.count({ where: { appId: Number(appId) } }),
]);
res.json({
appId: Number(appId),
installations: installations.map(install => ({
id: install.id,
token: install.token,
device: install.device,
note: install.note,
installedAt: install.installedAt,
updatedAt: install.updatedAt,
})),
total,
limit: parseInt(limit),
skip: parseInt(skip),
});
})
);
/**
* PUT /apps/devices/:deviceUuid/password
* 设置或更新设备密码
*
* Request Body:
* {
* "newPassword": "新密码",
* "passwordHint": "密码提示(可选)",
* "currentPassword": "当前密码(如果已设置密码则必须提供)"
* }
*/
router.put(
"/devices/:deviceUuid/password",
deviceInfoMiddleware,
errors.catchAsync(async (req, res, next) => {
const { newPassword, passwordHint, currentPassword } = req.body;
const device = res.locals.device;
if (!newPassword) {
return next(errors.createError(400, "请提供新密码"));
}
// 如果设备已有密码,必须先验证当前密码
if (device.password) {
if (!currentPassword) {
return next(errors.createError(401, "设备已设置密码,请提供当前密码"));
}
const isValid = await verifyDevicePassword(currentPassword, device.password);
if (!isValid) {
return next(errors.createError(401, "当前密码错误"));
}
}
// 哈希新密码
const hashedPassword = await hashPassword(newPassword);
// 更新设备密码
await prisma.device.update({
where: { id: device.id },
data: {
password: hashedPassword,
passwordHint: passwordHint || device.passwordHint,
},
});
res.json({
success: true,
message: device.password ? "密码已更新" : "密码已设置",
deviceUuid: device.uuid,
});
})
);
/**
* DELETE /apps/devices/:deviceUuid/password
* 删除设备密码
*
* Request Body:
* {
* "password": "当前密码(必须)"
* }
*/
router.delete(
"/devices/:deviceUuid/password",
deviceInfoMiddleware,
errors.catchAsync(async (req, res, next) => {
const { password } = req.body;
const device = res.locals.device;
if (!device.password) {
return next(errors.createError(400, "设备未设置密码"));
}
if (!password) {
return next(errors.createError(401, "请提供当前密码"));
}
// 验证密码
const isValid = await verifyDevicePassword(password, device.password);
if (!isValid) {
return next(errors.createError(401, "密码错误"));
}
// 删除密码
await prisma.device.update({
where: { id: device.id },
data: {
password: null,
passwordHint: null,
},
});
res.json({
success: true,
message: "密码已删除",
deviceUuid: device.uuid,
});
})
);
/**
* POST /apps/devices/:deviceUuid/password/verify
* 验证设备密码
*
* Request Body:
* {
* "password": "待验证的密码"
* }
*/
router.post(
"/devices/:deviceUuid/password/verify",
deviceInfoMiddleware,
errors.catchAsync(async (req, res, next) => {
const { password } = req.body;
const device = res.locals.device;
if (!device.password) {
return next(errors.createError(400, "设备未设置密码"));
}
if (!password) {
return next(errors.createError(400, "请提供密码"));
}
// 验证密码
const isValid = await verifyDevicePassword(password, device.password);
res.json({
valid: isValid,
});
})
);
export default router;