459 lines
15 KiB
TypeScript
459 lines
15 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { db } from "@/lib/db";
|
|
import { siteSettings, systemSettings } from "@/lib/db/schema";
|
|
import { eq } from "drizzle-orm";
|
|
import { auth } from "@/app/(auth)/auth";
|
|
import {
|
|
defaultMembershipConfig,
|
|
defaultMembershipUserOverrides,
|
|
normalizeMembershipConfig,
|
|
normalizeMembershipUserOverrides,
|
|
} from "@/lib/membership/config";
|
|
|
|
const HOME_VARIANT_KEY = "home_variant";
|
|
const HOME_SURPRISE_CATEGORIES_KEY = "home_surprise_categories";
|
|
const MEMBERSHIP_CONFIG_KEY = "membership_config";
|
|
const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
|
|
|
|
function normalizeHomeVariant(value: unknown): "classic" | "lovart" {
|
|
return value === "lovart" ? "lovart" : "classic";
|
|
}
|
|
|
|
function normalizeSurpriseCategories(value: unknown): string[] {
|
|
if (!Array.isArray(value)) {
|
|
return ["全部", "惊喜"];
|
|
}
|
|
|
|
const normalized = value
|
|
.map((item) => String(item || "").trim())
|
|
.filter(Boolean)
|
|
.slice(0, 12);
|
|
|
|
if (normalized.length === 0) {
|
|
return ["全部", "惊喜"];
|
|
}
|
|
|
|
const unique = Array.from(new Set(normalized));
|
|
if (!unique.includes("全部")) {
|
|
unique.unshift("全部");
|
|
}
|
|
|
|
return unique;
|
|
}
|
|
|
|
// GET /api/site-settings - 获取网站设置 (公开)
|
|
export async function GET() {
|
|
try {
|
|
const [
|
|
settings,
|
|
notificationSetting,
|
|
homeVariantSetting,
|
|
surpriseCategoriesSetting,
|
|
membershipConfigSetting,
|
|
membershipOverridesSetting,
|
|
] = await Promise.all([
|
|
db.select().from(siteSettings).limit(1).then(rows => rows[0]),
|
|
db
|
|
.select()
|
|
.from(systemSettings)
|
|
.where(eq(systemSettings.key, "login_notification"))
|
|
.limit(1)
|
|
.then(rows => rows[0]),
|
|
db
|
|
.select()
|
|
.from(systemSettings)
|
|
.where(eq(systemSettings.key, HOME_VARIANT_KEY))
|
|
.limit(1)
|
|
.then(rows => rows[0]),
|
|
db
|
|
.select()
|
|
.from(systemSettings)
|
|
.where(eq(systemSettings.key, HOME_SURPRISE_CATEGORIES_KEY))
|
|
.limit(1)
|
|
.then(rows => rows[0]),
|
|
db
|
|
.select()
|
|
.from(systemSettings)
|
|
.where(eq(systemSettings.key, MEMBERSHIP_CONFIG_KEY))
|
|
.limit(1)
|
|
.then(rows => rows[0]),
|
|
db
|
|
.select()
|
|
.from(systemSettings)
|
|
.where(eq(systemSettings.key, MEMBERSHIP_USER_OVERRIDES_KEY))
|
|
.limit(1)
|
|
.then(rows => rows[0]),
|
|
]);
|
|
|
|
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
|
|
const homeVariant = normalizeHomeVariant(homeVariantSetting?.value);
|
|
const surpriseCategories = normalizeSurpriseCategories(surpriseCategoriesSetting?.value);
|
|
const membershipConfig = normalizeMembershipConfig(membershipConfigSetting?.value);
|
|
const membershipUserOverrides = normalizeMembershipUserOverrides(
|
|
membershipOverridesSetting?.value
|
|
);
|
|
|
|
// 如果没有设置记录,返回默认值
|
|
const baseSettings = settings || {
|
|
siteName: "AI Chatbot",
|
|
siteLogo: null,
|
|
siteLogoDark: null,
|
|
favicon: null,
|
|
showAutoModel: true,
|
|
defaultTheme: "system",
|
|
usageTimeEnabled: false,
|
|
usageTimeStart: "08:00",
|
|
usageTimeEnd: "20:00",
|
|
usageTimeMessage: "由壹点累了也是需要休息的",
|
|
bannerEnabled: true,
|
|
bannerText: "升级会员, Nano Banana Pro 免费365天!",
|
|
bannerLink: null,
|
|
toolbox1Name: "工具箱",
|
|
toolbox1Enabled: true,
|
|
toolbox2Name: "工具箱2",
|
|
toolbox2Enabled: false,
|
|
tutorialVideoUrl: null,
|
|
tutorialEnabled: false,
|
|
rankingEnabled: true,
|
|
canvasEnabled: true,
|
|
canvasDefaultModelId: null,
|
|
canvasAutoModeDefaultModelId: null,
|
|
homeVariant,
|
|
surpriseCategories,
|
|
membershipConfig,
|
|
membershipUserOverrides,
|
|
};
|
|
|
|
const response = NextResponse.json({
|
|
...baseSettings,
|
|
notification,
|
|
homeVariant,
|
|
surpriseCategories,
|
|
membershipConfig,
|
|
membershipUserOverrides,
|
|
});
|
|
|
|
// 禁用缓存,确保总是获取最新设置
|
|
response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
|
response.headers.set("Pragma", "no-cache");
|
|
response.headers.set("Expires", "0");
|
|
|
|
return response;
|
|
} catch (error) {
|
|
console.error("[Site Settings API] Get error:", error);
|
|
return NextResponse.json(
|
|
{ error: "Failed to fetch site settings" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
const updateSchema = z.object({
|
|
siteName: z.string().min(1).max(128).optional(),
|
|
siteLogo: z.string().nullable().optional(),
|
|
siteLogoDark: z.string().nullable().optional(),
|
|
favicon: z.string().nullable().optional(),
|
|
showAutoModel: z.boolean().optional(),
|
|
defaultTheme: z.enum(["system", "light", "dark"]).optional(),
|
|
usageTimeEnabled: z.boolean().optional(),
|
|
usageTimeStart: z.string().length(5).optional(),
|
|
usageTimeEnd: z.string().length(5).optional(),
|
|
usageTimeMessage: z.string().optional(),
|
|
bannerEnabled: z.boolean().optional(),
|
|
bannerText: z.string().optional(),
|
|
bannerLink: z.string().nullable().optional(),
|
|
toolbox1Name: z.string().max(64).optional(),
|
|
toolbox1Enabled: z.boolean().optional(),
|
|
toolbox2Name: z.string().max(64).optional(),
|
|
toolbox2Enabled: z.boolean().optional(),
|
|
tutorialVideoUrl: z.string().nullable().optional(),
|
|
tutorialEnabled: z.boolean().optional(),
|
|
rankingEnabled: z.boolean().optional(),
|
|
canvasEnabled: z.boolean().optional(),
|
|
canvasDefaultModelId: z.string().nullable().optional(),
|
|
canvasAutoModeDefaultModelId: z.string().nullable().optional(),
|
|
homeVariant: z.enum(["classic", "lovart"]).optional(),
|
|
surpriseCategories: z.array(z.string().trim().min(1).max(20)).max(12).optional(),
|
|
membershipConfig: z.unknown().optional(),
|
|
membershipUserOverrides: z.unknown().optional(),
|
|
});
|
|
|
|
// PUT /api/site-settings - 更新网站设置(仅管理员)
|
|
export async function PUT(request: NextRequest) {
|
|
try {
|
|
const session = await auth();
|
|
|
|
if (!session?.user) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
if (session.user.type !== "admin") {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
const body = await request.json();
|
|
const data = updateSchema.parse(body);
|
|
const {
|
|
homeVariant,
|
|
surpriseCategories,
|
|
membershipConfig,
|
|
membershipUserOverrides,
|
|
...siteSettingsPayload
|
|
} = data;
|
|
|
|
// 查找现有设置
|
|
const [existing] = await db.select().from(siteSettings).limit(1);
|
|
|
|
if (existing) {
|
|
// 更新现有设置
|
|
const [updated] = await db
|
|
.update(siteSettings)
|
|
.set({
|
|
...siteSettingsPayload,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
})
|
|
.where(eq(siteSettings.id, existing.id))
|
|
.returning();
|
|
|
|
if (homeVariant !== undefined) {
|
|
await db
|
|
.insert(systemSettings)
|
|
.values({
|
|
key: HOME_VARIANT_KEY,
|
|
value: homeVariant,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: systemSettings.key,
|
|
set: {
|
|
value: homeVariant,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (surpriseCategories !== undefined) {
|
|
const normalized = normalizeSurpriseCategories(surpriseCategories);
|
|
await db
|
|
.insert(systemSettings)
|
|
.values({
|
|
key: HOME_SURPRISE_CATEGORIES_KEY,
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: systemSettings.key,
|
|
set: {
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (membershipConfig !== undefined) {
|
|
const normalized = normalizeMembershipConfig(membershipConfig);
|
|
await db
|
|
.insert(systemSettings)
|
|
.values({
|
|
key: MEMBERSHIP_CONFIG_KEY,
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: systemSettings.key,
|
|
set: {
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (membershipUserOverrides !== undefined) {
|
|
const normalized = normalizeMembershipUserOverrides(membershipUserOverrides);
|
|
await db
|
|
.insert(systemSettings)
|
|
.values({
|
|
key: MEMBERSHIP_USER_OVERRIDES_KEY,
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: systemSettings.key,
|
|
set: {
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({
|
|
...updated,
|
|
homeVariant: homeVariant ?? normalizeHomeVariant(undefined),
|
|
surpriseCategories:
|
|
surpriseCategories !== undefined
|
|
? normalizeSurpriseCategories(surpriseCategories)
|
|
: normalizeSurpriseCategories(undefined),
|
|
membershipConfig:
|
|
membershipConfig !== undefined
|
|
? normalizeMembershipConfig(membershipConfig)
|
|
: defaultMembershipConfig,
|
|
membershipUserOverrides:
|
|
membershipUserOverrides !== undefined
|
|
? normalizeMembershipUserOverrides(membershipUserOverrides)
|
|
: defaultMembershipUserOverrides,
|
|
});
|
|
} else {
|
|
// 创建新设置
|
|
const [created] = await db
|
|
.insert(siteSettings)
|
|
.values({
|
|
siteName: siteSettingsPayload.siteName || "AI Chatbot",
|
|
siteLogo: siteSettingsPayload.siteLogo,
|
|
siteLogoDark: siteSettingsPayload.siteLogoDark,
|
|
favicon: siteSettingsPayload.favicon,
|
|
showAutoModel: siteSettingsPayload.showAutoModel ?? true,
|
|
defaultTheme: siteSettingsPayload.defaultTheme ?? "system",
|
|
usageTimeEnabled: siteSettingsPayload.usageTimeEnabled ?? false,
|
|
usageTimeStart: siteSettingsPayload.usageTimeStart ?? "08:00",
|
|
usageTimeEnd: siteSettingsPayload.usageTimeEnd ?? "20:00",
|
|
usageTimeMessage: siteSettingsPayload.usageTimeMessage ?? "由壹点累了也是需要休息的",
|
|
bannerEnabled: siteSettingsPayload.bannerEnabled ?? true,
|
|
bannerText: siteSettingsPayload.bannerText ?? "升级会员, Nano Banana Pro 免费365天!",
|
|
bannerLink: siteSettingsPayload.bannerLink,
|
|
toolbox1Name: siteSettingsPayload.toolbox1Name ?? "工具箱",
|
|
toolbox1Enabled: siteSettingsPayload.toolbox1Enabled ?? true,
|
|
toolbox2Name: siteSettingsPayload.toolbox2Name ?? "工具箱2",
|
|
toolbox2Enabled: siteSettingsPayload.toolbox2Enabled ?? false,
|
|
tutorialVideoUrl: siteSettingsPayload.tutorialVideoUrl,
|
|
tutorialEnabled: siteSettingsPayload.tutorialEnabled ?? false,
|
|
rankingEnabled: siteSettingsPayload.rankingEnabled ?? true,
|
|
canvasEnabled: siteSettingsPayload.canvasEnabled ?? true,
|
|
canvasDefaultModelId: siteSettingsPayload.canvasDefaultModelId,
|
|
canvasAutoModeDefaultModelId: siteSettingsPayload.canvasAutoModeDefaultModelId,
|
|
updatedBy: session.user.id,
|
|
})
|
|
.returning();
|
|
|
|
if (homeVariant !== undefined) {
|
|
await db
|
|
.insert(systemSettings)
|
|
.values({
|
|
key: HOME_VARIANT_KEY,
|
|
value: homeVariant,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: systemSettings.key,
|
|
set: {
|
|
value: homeVariant,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (surpriseCategories !== undefined) {
|
|
const normalized = normalizeSurpriseCategories(surpriseCategories);
|
|
await db
|
|
.insert(systemSettings)
|
|
.values({
|
|
key: HOME_SURPRISE_CATEGORIES_KEY,
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: systemSettings.key,
|
|
set: {
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (membershipConfig !== undefined) {
|
|
const normalized = normalizeMembershipConfig(membershipConfig);
|
|
await db
|
|
.insert(systemSettings)
|
|
.values({
|
|
key: MEMBERSHIP_CONFIG_KEY,
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: systemSettings.key,
|
|
set: {
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (membershipUserOverrides !== undefined) {
|
|
const normalized = normalizeMembershipUserOverrides(membershipUserOverrides);
|
|
await db
|
|
.insert(systemSettings)
|
|
.values({
|
|
key: MEMBERSHIP_USER_OVERRIDES_KEY,
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: systemSettings.key,
|
|
set: {
|
|
value: normalized,
|
|
updatedAt: new Date(),
|
|
updatedBy: session.user.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({
|
|
...created,
|
|
homeVariant: homeVariant ?? normalizeHomeVariant(undefined),
|
|
surpriseCategories:
|
|
surpriseCategories !== undefined
|
|
? normalizeSurpriseCategories(surpriseCategories)
|
|
: normalizeSurpriseCategories(undefined),
|
|
membershipConfig:
|
|
membershipConfig !== undefined
|
|
? normalizeMembershipConfig(membershipConfig)
|
|
: defaultMembershipConfig,
|
|
membershipUserOverrides:
|
|
membershipUserOverrides !== undefined
|
|
? normalizeMembershipUserOverrides(membershipUserOverrides)
|
|
: defaultMembershipUserOverrides,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("[Site Settings API] Update error:", error);
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid request data", details: error.errors },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{ error: "Failed to update site settings" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|