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"; 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() { try { const session = await auth(); const isAdmin = session?.user?.type === "admin"; const [settings] = await db.select().from(authSettings).limit(1); 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 jsonNoStore( { error: "Failed to fetch auth settings" }, { status: 500 } ); } } 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 } ); } }