@@ -7,27 +7,31 @@
|
||||
// - 此接口不返回任何用户数据
|
||||
|
||||
import { setResponseStatus } from "h3";
|
||||
import { getSysConfig, maskApiKey } from "~~/server/utils/sysConfig";
|
||||
|
||||
import { apiErr, apiOk } from "~~/server/utils/response";
|
||||
import { getSysConfig, maskApiKey } from "~~/server/utils/sysConfig";
|
||||
import type { ISystemConfigResponse } from "~~/shared/types/settings";
|
||||
|
||||
export default defineEventHandler(async (event): Promise<ISystemConfigResponse> => {
|
||||
// 二次校验:必须是 superadmin 才能访问系统配置
|
||||
const role = (event.context.auth?.user as { role?: string } | undefined)
|
||||
?.role;
|
||||
if (role !== "superadmin") {
|
||||
setResponseStatus(event, 403);
|
||||
return apiErr(403, "权限不足") as ISystemConfigResponse;
|
||||
export default defineEventHandler(
|
||||
async (event): Promise<ISystemConfigResponse> => {
|
||||
// 二次校验:必须是 superadmin 才能访问系统配置
|
||||
const role = (event.context.auth?.user as { role?: string } | undefined)
|
||||
?.role;
|
||||
if (role !== "superadmin") {
|
||||
setResponseStatus(event, 403);
|
||||
return apiErr(403, "权限不足") as unknown as ISystemConfigResponse;
|
||||
}
|
||||
|
||||
const cfg = await getSysConfig();
|
||||
|
||||
return apiOk({
|
||||
openAiApiBase: cfg.openAiApiBase,
|
||||
openAiApiKeyPreview: maskApiKey(cfg.openAiApiKey),
|
||||
openAiModel: cfg.openAiModel,
|
||||
maxTokens: cfg.maxTokens,
|
||||
temperature: cfg.temperature,
|
||||
maxSseLineBytes: cfg.maxSseLineBytes,
|
||||
allowRegistration: cfg.allowRegistration
|
||||
});
|
||||
}
|
||||
|
||||
const cfg = await getSysConfig();
|
||||
|
||||
return apiOk({
|
||||
openAiApiBase: cfg.openAiApiBase,
|
||||
openAiApiKeyPreview: maskApiKey(cfg.openAiApiKey),
|
||||
openAiModel: cfg.openAiModel,
|
||||
maxTokens: cfg.maxTokens,
|
||||
temperature: cfg.temperature,
|
||||
maxSseLineBytes: cfg.maxSseLineBytes
|
||||
});
|
||||
});
|
||||
);
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
// - 更新成功后立即失效系统配置缓存,下次请求即生效
|
||||
|
||||
import { readBody, setResponseStatus } from "h3";
|
||||
|
||||
import { prisma } from "~~/server/utils/db";
|
||||
import { createApiLogger, toSafeLogError } from "~~/server/utils/logging";
|
||||
import { apiErr, apiOk } from "~~/server/utils/response";
|
||||
import {
|
||||
invalidateSysConfigCache
|
||||
} from "~~/server/utils/sysConfig";
|
||||
import { invalidateSysConfigCache } from "~~/server/utils/sysConfig";
|
||||
import type { ISystemConfigUpdateRequest } from "~~/shared/types/settings";
|
||||
|
||||
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
||||
@@ -25,7 +24,8 @@ const SYS_KEY = {
|
||||
openAiModel: "openai_model",
|
||||
maxTokens: "max_tokens",
|
||||
temperature: "temperature",
|
||||
maxSseLineBytes: "openai_stream_max_sse_line_bytes"
|
||||
maxSseLineBytes: "openai_stream_max_sse_line_bytes",
|
||||
allowRegistration: "allow_registration"
|
||||
} as const;
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -63,6 +63,9 @@ export default defineEventHandler(async (event) => {
|
||||
if (typeof body.maxSseLineBytes === "number") {
|
||||
req.maxSseLineBytes = body.maxSseLineBytes;
|
||||
}
|
||||
if (typeof body.allowRegistration === "boolean") {
|
||||
req.allowRegistration = body.allowRegistration;
|
||||
}
|
||||
|
||||
// 构建需要写入的 KV 对(key 为空时跳过)
|
||||
type SysKv = { key: string; value: string };
|
||||
@@ -90,13 +93,19 @@ export default defineEventHandler(async (event) => {
|
||||
value: String(req.maxSseLineBytes)
|
||||
});
|
||||
}
|
||||
if (req.allowRegistration !== undefined) {
|
||||
upserts.push({
|
||||
key: SYS_KEY.allowRegistration,
|
||||
value: String(req.allowRegistration)
|
||||
});
|
||||
}
|
||||
|
||||
if (upserts.length === 0) {
|
||||
return apiOk(null);
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.$transaction(
|
||||
await Promise.all(
|
||||
upserts.map((item) =>
|
||||
prisma.systemConfig.upsert({
|
||||
where: { key: item.key },
|
||||
@@ -115,7 +124,9 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
return apiOk(null);
|
||||
} catch (error) {
|
||||
logger.error("system_config_update_failed", { error: toSafeLogError(error) });
|
||||
logger.error("system_config_update_failed", {
|
||||
error: toSafeLogError(error)
|
||||
});
|
||||
setResponseStatus(event, 500);
|
||||
return apiErr(500, "服务器内部错误");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// server/api/auth/register-open.get.ts - 公开接口:查询当前是否开放注册
|
||||
//
|
||||
// 该接口无需登录,供前端注册页面展示禁用提示使用。
|
||||
// 路径位于 /api/auth/** 公开区内,不经过鉴权中间件。
|
||||
// 不返回任何用户数据或系统敏感信息。
|
||||
|
||||
import { apiOk } from "~~/server/utils/response";
|
||||
import { getSysConfig } from "~~/server/utils/sysConfig";
|
||||
import type { IRegisterStatusResponse } from "~~/shared/types/settings";
|
||||
|
||||
export default defineEventHandler(
|
||||
async (): Promise<IRegisterStatusResponse> => {
|
||||
const cfg = await getSysConfig();
|
||||
return apiOk({ allowRegistration: cfg.allowRegistration });
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
// server/middleware/registration-guard.ts - 注册开关守卫
|
||||
//
|
||||
// 拦截 POST /api/auth/sign-up/email,若系统配置关闭了注册,立即返回 403。
|
||||
// 此中间件优先于 Better Auth catch-all handler 执行,无需改动 auth.ts。
|
||||
|
||||
import { getRequestURL, setResponseStatus } from "h3";
|
||||
|
||||
import { apiErr } from "~~/server/utils/response";
|
||||
import { getSysConfig } from "~~/server/utils/sysConfig";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { pathname } = getRequestURL(event);
|
||||
|
||||
// 只拦截注册接口
|
||||
if (pathname !== "/api/auth/sign-up/email" || event.method !== "POST") return;
|
||||
|
||||
const cfg = await getSysConfig();
|
||||
if (!cfg.allowRegistration) {
|
||||
setResponseStatus(event, 403);
|
||||
return apiErr(403, "当前未开启注册");
|
||||
}
|
||||
});
|
||||
@@ -17,6 +17,8 @@ export interface SysConfig {
|
||||
temperature: number;
|
||||
/** 单行 SSE data 最大字节数,防止异常流无限堆内存 */
|
||||
maxSseLineBytes: number;
|
||||
/** 是否允许新用户注册;false 时注册接口直接拒绝 */
|
||||
allowRegistration: boolean;
|
||||
}
|
||||
|
||||
// system_config 表 key 字段的常量映射
|
||||
@@ -26,7 +28,8 @@ const CONFIG_KEYS = {
|
||||
openAiModel: "openai_model",
|
||||
maxTokens: "max_tokens",
|
||||
temperature: "temperature",
|
||||
maxSseLineBytes: "openai_stream_max_sse_line_bytes"
|
||||
maxSseLineBytes: "openai_stream_max_sse_line_bytes",
|
||||
allowRegistration: "allow_registration"
|
||||
} as const;
|
||||
|
||||
// 硬编码默认值,与原 env.ts 的 fallback 保持一致
|
||||
@@ -36,7 +39,9 @@ const DEFAULTS: SysConfig = {
|
||||
openAiModel: "gpt-5.2",
|
||||
maxTokens: 500,
|
||||
temperature: 0.7,
|
||||
maxSseLineBytes: 1_048_576
|
||||
maxSseLineBytes: 1_048_576,
|
||||
// 默认开放注册;管理员可通过系统配置关闭
|
||||
allowRegistration: true
|
||||
};
|
||||
|
||||
// 内存缓存:减少每请求查 DB 的开销
|
||||
@@ -82,7 +87,10 @@ export const getSysConfig = async (): Promise<SysConfig> => {
|
||||
openAiModel: str(CONFIG_KEYS.openAiModel, DEFAULTS.openAiModel),
|
||||
maxTokens: int(CONFIG_KEYS.maxTokens, DEFAULTS.maxTokens),
|
||||
temperature: float(CONFIG_KEYS.temperature, DEFAULTS.temperature),
|
||||
maxSseLineBytes: int(CONFIG_KEYS.maxSseLineBytes, DEFAULTS.maxSseLineBytes)
|
||||
maxSseLineBytes: int(CONFIG_KEYS.maxSseLineBytes, DEFAULTS.maxSseLineBytes),
|
||||
// "true" 以外的值均视为关闭;空值(未设置)走默认值 true
|
||||
allowRegistration:
|
||||
(map.get(CONFIG_KEYS.allowRegistration) ?? "true") !== "false"
|
||||
};
|
||||
|
||||
_cachedConfig = config;
|
||||
@@ -144,7 +152,9 @@ export const getUserEffectiveApiConfig = async (
|
||||
openAiModel: customModel.length > 0 ? customModel : sysCfg.openAiModel,
|
||||
maxTokens: userConfig.customMaxTokens ?? sysCfg.maxTokens,
|
||||
temperature: userConfig.customTemperature ?? sysCfg.temperature,
|
||||
maxSseLineBytes: sysCfg.maxSseLineBytes
|
||||
maxSseLineBytes: sysCfg.maxSseLineBytes,
|
||||
// 用户有效配置不改变注册开关,手带系统值
|
||||
allowRegistration: sysCfg.allowRegistration
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user