2026-05-26 20:10:36 +08:00

296 lines
10 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { db } from "@/lib/db";
import { authSettings } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { auth } from "@/app/(auth)/auth";
import { publicJson, withTimeout } from "@/lib/public-api/performance";
const PUBLIC_AUTH_SETTINGS_FALLBACK = {
dingTalkAppKey: "",
isDingTalkEnabled: false,
isGoogleEnabled: false,
authUrl: process.env.AUTH_URL || "",
isGoogleConfigured: false,
isRegistrationEnabled: true,
isEmailVerificationEnabled: false,
emailVerificationExpiresMinutes: 10,
isEmailServiceConfigured: false,
dingTalkOrganizations: [],
organizations: [],
};
function jsonNoStore(data: unknown, init?: ResponseInit) {
const response = NextResponse.json(data, init);
response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
response.headers.set("Pragma", "no-cache");
response.headers.set("Expires", "0");
response.headers.set("Vary", "Cookie, Authorization");
return response;
}
// GET /api/auth-settings - 获取认证配置
export async function GET() {
const startedAt = Date.now();
try {
const session = await auth();
const isAdmin = session?.user?.type === "admin";
const [settings] = await withTimeout(
db.select().from(authSettings).limit(1),
1600,
"auth-settings"
);
const resolvedGoogleClientId =
settings?.googleClientId || process.env.GOOGLE_CLIENT_ID || "";
const resolvedGoogleClientSecret =
settings?.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET || "";
const resolvedAuthUrl = settings?.authUrl || process.env.AUTH_URL || "";
const isGoogleConfigured = Boolean(
resolvedGoogleClientId && resolvedGoogleClientSecret
);
// 如果没有配置记录,返回默认值
if (!settings) {
return jsonNoStore({
dingTalkAppKey: "",
dingTalkAppSecret: "", // 即使是空的,非管理员也不应该看到这个字段名,但因为是空字符串,问题不大
dingTalkOrganizations: [],
organizations: [],
isDingTalkEnabled: false,
isGoogleEnabled: false,
googleClientId: "",
googleClientSecret: "",
authUrl: process.env.AUTH_URL || "",
isGoogleConfigured,
isRegistrationEnabled: true,
isEmailVerificationEnabled: false,
emailVerificationExpiresMinutes: 10,
resendApiKey: "",
resendFromEmail: "",
isEmailServiceConfigured: false,
});
}
if (isAdmin) {
const organizations =
settings.dingTalkOrganizations?.map((org) => ({
name: org.name,
})) ?? [];
return jsonNoStore({
...settings,
googleClientId: resolvedGoogleClientId,
googleClientSecret: resolvedGoogleClientSecret,
authUrl: resolvedAuthUrl,
isGoogleConfigured,
dingTalkOrganizations: settings.dingTalkOrganizations ?? [],
organizations,
isEmailVerificationEnabled: settings.isEmailVerificationEnabled,
emailVerificationExpiresMinutes: settings.emailVerificationExpiresMinutes,
resendApiKey: settings.resendApiKey || "",
resendFromEmail: settings.resendFromEmail || "",
isEmailServiceConfigured: Boolean(
(settings.resendApiKey || "").trim() && (settings.resendFromEmail || "").trim()
),
});
}
// 非管理员,脱敏返回,只暴露组织名称
const organizations =
settings.dingTalkOrganizations?.map((org) => ({
name: org.name,
})) ?? [];
return jsonNoStore({
dingTalkAppKey: settings.dingTalkAppKey,
isDingTalkEnabled: settings.isDingTalkEnabled,
isGoogleEnabled: settings.isGoogleEnabled,
authUrl: resolvedAuthUrl,
isGoogleConfigured,
isRegistrationEnabled: settings.isRegistrationEnabled,
isEmailVerificationEnabled: settings.isEmailVerificationEnabled,
emailVerificationExpiresMinutes: settings.emailVerificationExpiresMinutes,
isEmailServiceConfigured: Boolean(
(settings.resendApiKey || "").trim() && (settings.resendFromEmail || "").trim()
),
dingTalkOrganizations: organizations,
organizations,
// 不返回 dingTalkAppSecret 以及各组织的密钥
});
} catch (error) {
console.error("[Auth Settings API] Get error:", error);
return publicJson(PUBLIC_AUTH_SETTINGS_FALLBACK, {
cacheStatus: "FALLBACK",
cacheControl: "no-store, no-cache, must-revalidate, proxy-revalidate",
fallback: true,
startedAt,
});
}
}
const orgSchema = z.object({
name: z.string().min(1),
appKey: z.string().min(1),
appSecret: z.string().min(1),
});
const updateSchema = z.object({
dingTalkAppKey: z.string().optional(),
dingTalkAppSecret: z.string().optional(),
dingTalkOrganizations: z.array(orgSchema).optional(),
isDingTalkEnabled: z.boolean().optional(),
isGoogleEnabled: z.boolean().optional(),
googleClientId: z.string().optional(),
googleClientSecret: z.string().optional(),
authUrl: z.string().optional(),
isRegistrationEnabled: z.boolean().optional(),
isEmailVerificationEnabled: z.boolean().optional(),
emailVerificationExpiresMinutes: z.number().int().min(3).max(30).optional(),
resendApiKey: z.string().optional(),
resendFromEmail: z.string().optional(),
});
function resolveKeepExistingOnEmpty(params: {
incoming?: string;
existing?: string | null;
defaultValue?: string;
}) {
if (params.incoming === undefined) {
return params.existing ?? params.defaultValue ?? "";
}
const trimmed = params.incoming.trim();
if (trimmed.length === 0) {
return params.existing ?? params.defaultValue ?? "";
}
return trimmed;
}
// PUT /api/auth-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 [existing] = await db.select().from(authSettings).limit(1);
const organizations = data.dingTalkOrganizations ?? existing?.dingTalkOrganizations ?? [];
const primaryOrg = organizations[0] as
| { name: string; appKey: string; appSecret: string }
| undefined;
const resolvedAppKey = resolveKeepExistingOnEmpty({
incoming: data.dingTalkAppKey,
existing: primaryOrg?.appKey ?? existing?.dingTalkAppKey ?? null,
defaultValue: "",
});
const resolvedAppSecret = resolveKeepExistingOnEmpty({
incoming: data.dingTalkAppSecret,
existing: primaryOrg?.appSecret ?? existing?.dingTalkAppSecret ?? null,
defaultValue: "",
});
const resolvedGoogleClientId = resolveKeepExistingOnEmpty({
incoming: data.googleClientId,
existing: existing?.googleClientId,
defaultValue: "",
});
const resolvedGoogleClientSecret = resolveKeepExistingOnEmpty({
incoming: data.googleClientSecret,
existing: existing?.googleClientSecret,
defaultValue: "",
});
const resolvedAuthUrl = data.authUrl?.trim() ?? existing?.authUrl ?? "";
const resolvedResendApiKey = resolveKeepExistingOnEmpty({
incoming: data.resendApiKey,
existing: existing?.resendApiKey,
defaultValue: "",
});
const resolvedResendFromEmail = resolveKeepExistingOnEmpty({
incoming: data.resendFromEmail,
existing: existing?.resendFromEmail,
defaultValue: "",
});
if (existing) {
// 更新现有配置
const [updated] = await db
.update(authSettings)
.set({
dingTalkAppKey: resolvedAppKey,
dingTalkAppSecret: resolvedAppSecret,
googleClientId: resolvedGoogleClientId,
googleClientSecret: resolvedGoogleClientSecret,
authUrl: resolvedAuthUrl,
dingTalkOrganizations: organizations,
isDingTalkEnabled:
data.isDingTalkEnabled ?? existing.isDingTalkEnabled,
isGoogleEnabled: data.isGoogleEnabled ?? existing.isGoogleEnabled,
isRegistrationEnabled:
data.isRegistrationEnabled ?? existing.isRegistrationEnabled,
isEmailVerificationEnabled:
data.isEmailVerificationEnabled ?? existing.isEmailVerificationEnabled,
emailVerificationExpiresMinutes:
data.emailVerificationExpiresMinutes ??
existing.emailVerificationExpiresMinutes,
resendApiKey: resolvedResendApiKey,
resendFromEmail: resolvedResendFromEmail,
updatedAt: new Date(),
updatedBy: session.user.id,
})
.where(eq(authSettings.id, existing.id))
.returning();
return NextResponse.json(updated);
}
// 创建新配置
const [created] = await db
.insert(authSettings)
.values({
dingTalkAppKey: resolvedAppKey,
dingTalkAppSecret: resolvedAppSecret,
googleClientId: resolvedGoogleClientId,
googleClientSecret: resolvedGoogleClientSecret,
authUrl: resolvedAuthUrl,
dingTalkOrganizations: organizations,
isDingTalkEnabled: data.isDingTalkEnabled || false,
isGoogleEnabled: data.isGoogleEnabled || false,
isRegistrationEnabled: data.isRegistrationEnabled ?? true,
isEmailVerificationEnabled: data.isEmailVerificationEnabled ?? false,
emailVerificationExpiresMinutes:
data.emailVerificationExpiresMinutes ?? 10,
resendApiKey: resolvedResendApiKey,
resendFromEmail: resolvedResendFromEmail,
updatedBy: session.user.id,
})
.returning();
return NextResponse.json(created);
} catch (error) {
console.error("[Auth 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 auth settings" },
{ status: 500 }
);
}
}