139 lines
4.4 KiB
TypeScript
139 lines
4.4 KiB
TypeScript
// server/api/user/settings.put.ts - 更新当前用户的自定义 API 配置
|
|
//
|
|
// 流程:鉴权 → 白名单校验 → 合并并写入 user_config 单行 JSON value → 返回
|
|
// 安全边界:
|
|
// - customApiKey 为空字符串时跳过,不清空已有 key
|
|
// - 只接受声明字段,拒绝未知字段
|
|
// - 不读取或返回系统配置任何内容
|
|
// 注意:保存时使用单条 SQL upsert,避免多次写入造成连接占用
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
import { readBody, setResponseStatus } from "h3";
|
|
|
|
import { Prisma } from "~~/prisma/generated/client";
|
|
import { prisma } from "~~/server/utils/db";
|
|
import { createApiLogger, toSafeLogError } from "~~/server/utils/logging";
|
|
import { apiErr, apiOk } from "~~/server/utils/response";
|
|
import {
|
|
hasUserConfigMutation,
|
|
mergeUserConfigUpdate,
|
|
normalizeUserConfigValue,
|
|
toUserConfigJson
|
|
} from "~~/server/utils/userConfig";
|
|
import type { IUserSettingsUpdateRequest } from "~~/shared/types/settings";
|
|
|
|
/** 是否是纯 JSON 对象 */
|
|
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
|
const ALLOWED_KEYS = new Set([
|
|
"useCustomConfig",
|
|
"customApiBase",
|
|
"customApiKey",
|
|
"customModel",
|
|
"customMaxTokens",
|
|
"customTemperature"
|
|
]);
|
|
|
|
/** 写入用户配置单行 JSON value */
|
|
const writeUserConfigValue = async (
|
|
userId: string,
|
|
value: Prisma.InputJsonObject
|
|
) => {
|
|
const updatedAt = new Date();
|
|
const jsonValue = JSON.stringify(value);
|
|
|
|
await prisma.$executeRaw(Prisma.sql`
|
|
INSERT INTO user_config (id, userId, value, updatedAt)
|
|
VALUES (${randomUUID()}, ${userId}, ${jsonValue}, ${updatedAt})
|
|
ON DUPLICATE KEY UPDATE value = VALUES(value), updatedAt = VALUES(updatedAt)
|
|
`);
|
|
};
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const logger = createApiLogger(event, "user.settings.put");
|
|
const userId = event.context.auth!.user.id;
|
|
|
|
const body = await readBody<unknown>(event).catch(() => null);
|
|
if (!isRecord(body)) {
|
|
setResponseStatus(event, 400);
|
|
return apiErr(400, "请求体必须是 JSON 对象");
|
|
}
|
|
const unknownKeys = Object.keys(body).filter((key) => !ALLOWED_KEYS.has(key));
|
|
if (unknownKeys.length > 0) {
|
|
setResponseStatus(event, 400);
|
|
return apiErr(400, "请求体包含不支持的字段");
|
|
}
|
|
|
|
// 白名单字段提取
|
|
const req: IUserSettingsUpdateRequest = {};
|
|
if (typeof body.useCustomConfig === "boolean") {
|
|
req.useCustomConfig = body.useCustomConfig;
|
|
}
|
|
if (typeof body.customApiBase === "string") {
|
|
req.customApiBase = body.customApiBase.trim();
|
|
}
|
|
if (typeof body.customApiKey === "string") {
|
|
req.customApiKey = body.customApiKey; // 空字符串 = 不更新,见下方逻辑
|
|
}
|
|
if (typeof body.customModel === "string") {
|
|
req.customModel = body.customModel.trim();
|
|
}
|
|
if (
|
|
typeof body.customMaxTokens === "number" &&
|
|
Number.isFinite(body.customMaxTokens)
|
|
) {
|
|
req.customMaxTokens = body.customMaxTokens;
|
|
} else if (body.customMaxTokens === null) {
|
|
req.customMaxTokens = null;
|
|
}
|
|
if (
|
|
typeof body.customTemperature === "number" &&
|
|
Number.isFinite(body.customTemperature)
|
|
) {
|
|
req.customTemperature = body.customTemperature;
|
|
} else if (body.customTemperature === null) {
|
|
req.customTemperature = null;
|
|
}
|
|
|
|
// 没有任何变更时直接返回
|
|
if (!hasUserConfigMutation(req)) {
|
|
return apiOk(null);
|
|
}
|
|
|
|
try {
|
|
const currentRow = await prisma.userConfig.findUnique({
|
|
where: { userId },
|
|
select: { value: true }
|
|
});
|
|
const current = normalizeUserConfigValue(currentRow?.value);
|
|
const next = mergeUserConfigUpdate(current, req);
|
|
const jsonValue = toUserConfigJson(next);
|
|
|
|
logger.info("user_settings_write_start", {
|
|
hasExistingRow: Boolean(currentRow),
|
|
useCustomConfig: next.useCustomConfig
|
|
});
|
|
|
|
await writeUserConfigValue(userId, jsonValue);
|
|
|
|
logger.info("user_settings_updated", {
|
|
useCustomConfig: next.useCustomConfig,
|
|
hasCustomApiBase: Boolean(next.customApiBase),
|
|
hasCustomApiKey: Boolean(next.customApiKey),
|
|
hasCustomModel: Boolean(next.customModel),
|
|
hasCustomMaxTokens: next.customMaxTokens !== null,
|
|
hasCustomTemperature: next.customTemperature !== null
|
|
});
|
|
|
|
return apiOk(null);
|
|
} catch (error) {
|
|
logger.error("user_settings_update_failed", {
|
|
error: toSafeLogError(error)
|
|
});
|
|
setResponseStatus(event, 500);
|
|
return apiErr(500, "服务器内部错误");
|
|
}
|
|
});
|