296 lines
10 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|