fix(admin): secure proxied model mutations
Staging Backend Quality Gate / validate (push) Successful in 19m41s
Staging Backend Quality Gate / publish (push) Successful in 28m13s

This commit is contained in:
Jesse_Chen
2026-08-08 01:26:21 +08:00
parent c30b4b906b
commit 99e2abe9bc
9 changed files with 275 additions and 58 deletions
+13 -5
View File
@@ -28,7 +28,6 @@ const providerSchema = z.object({
baseUrl: z.string().url().startsWith("https://").nullable().optional(),
apiKey: z.string().max(4096).optional(),
enabled: z.boolean(),
reason: z.string().trim().min(1).max(500),
}).strict();
const settingsSchema = z.record(z.string(), z.unknown()).superRefine((value, context) => {
if (modelSettingsContainSecrets(value)) {
@@ -52,20 +51,26 @@ const draftSchema = z.object({
isDefault: z.boolean(),
fallbackModelId: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/).nullable().optional(),
settings: settingsSchema.default({}),
reason: z.string().trim().min(1).max(500),
}).strict();
const actionSchema = z.discriminatedUnion("action", [
providerSchema,
draftSchema,
z.object({ action: z.literal("test"), versionId: z.string().uuid() }).strict(),
z.object({ action: z.literal("publish"), versionId: z.string().uuid(), reason: z.string().trim().min(1).max(500) }).strict(),
z.object({ action: z.literal("rollback"), configId: z.string().uuid(), targetVersion: z.number().int().positive(), reason: z.string().trim().min(1).max(500) }).strict(),
z.object({ action: z.literal("publish"), versionId: z.string().uuid() }).strict(),
z.object({ action: z.literal("rollback"), configId: z.string().uuid(), targetVersion: z.number().int().positive() }).strict(),
]).superRefine((value, context) => {
if (value.action === "saveDraft" && value.isDefault && !value.enabled) {
context.addIssue({ code: "custom", path: ["isDefault"], message: "默认模型必须启用" });
}
});
const modelMutationAuditReasons = {
saveProvider: "保存模型供应商",
saveDraft: "保存模型草稿",
publish: "发布模型版本",
rollback: "回滚模型版本",
} as const;
type ProviderRow = {
id: string;
code: string;
@@ -178,8 +183,11 @@ export async function POST(request: Request) {
: "models.write";
const session = await requireAdminMutation(request, permission);
const rid = requestId(request);
const mutation: AdminModelMutation = body.data.action === "test"
? body.data
: { ...body.data, reason: modelMutationAuditReasons[body.data.action] };
return await handleAdminModelMutation(
body.data as AdminModelMutation,
mutation,
{ actorUserId: session.user.id, requestId: rid },
{
queryRows: (sql, values) => queryAdminRows<Record<string, unknown>>(sql, values),