// 系统设置 API import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import type { Prisma } from '@prisma/client'; import { authOptions } from '@/lib/auth'; import { prisma } from '@/lib/prisma'; import { isSystemAdmin } from '@/lib/department-permission'; import { proxyHttpUrl } from '@/lib/url-rewrite'; import { DEFAULT_HEADER_LOGO_URL, DEFAULT_LOGIN_LOGO_URL, DEFAULT_SITE_NAME, normalizeBrandLogoUrl, } from '@/lib/brand-assets'; const DEFAULT_HEADER_BG_COLOR = '#1f2937'; const NO_STORE_HEADERS = { 'Cache-Control': 'no-store, no-cache, max-age=0, must-revalidate', Pragma: 'no-cache', Expires: '0', }; const systemSettingsSelect = { id: true, headerBgColor: true, siteName: true, headerLogoUrl: true, loginLogoUrl: true, s3Endpoint: true, s3Bucket: true, s3Region: true, s3PublicUrl: true, maintenanceEnabled: true, maintenanceMessage: true, maintenanceStartTime: true, maintenanceEndTime: true, } as const; type SystemSettingsRecord = Prisma.SystemSettingsGetPayload<{ select: typeof systemSettingsSelect; }>; type SettingsSessionUser = { id?: string | null; role?: string | null; isSuperAdmin?: boolean | null; }; function jsonNoStore(body: unknown, status = 200) { return NextResponse.json(body, { status, headers: NO_STORE_HEADERS, }); } function canManageSystemSettings(user: SettingsSessionUser | null | undefined) { return Boolean( user?.id && (isSystemAdmin(user.role) || user.isSuperAdmin === true), ); } function toPublicSettingsDto(settings: SystemSettingsRecord | null) { const headerLogoUrl = normalizeBrandLogoUrl(settings?.headerLogoUrl) || DEFAULT_HEADER_LOGO_URL; const loginLogoUrl = normalizeBrandLogoUrl(settings?.loginLogoUrl) || DEFAULT_LOGIN_LOGO_URL; return { headerBgColor: settings?.headerBgColor || DEFAULT_HEADER_BG_COLOR, siteName: settings?.siteName || DEFAULT_SITE_NAME, headerLogoUrl: proxyHttpUrl(headerLogoUrl) || headerLogoUrl, loginLogoUrl: proxyHttpUrl(loginLogoUrl) || loginLogoUrl, maintenanceEnabled: settings?.maintenanceEnabled ?? false, maintenanceMessage: settings?.maintenanceMessage ?? null, maintenanceStartTime: settings?.maintenanceStartTime ?? null, maintenanceEndTime: settings?.maintenanceEndTime ?? null, }; } function toAdminSettingsDto( settings: SystemSettingsRecord | null, s3CredentialsConfigured: boolean, ) { const normalizedStoredHeaderLogoUrl = normalizeBrandLogoUrl(settings?.headerLogoUrl); const normalizedStoredLoginLogoUrl = normalizeBrandLogoUrl(settings?.loginLogoUrl); const headerLogoUrl = normalizedStoredHeaderLogoUrl || DEFAULT_HEADER_LOGO_URL; const loginLogoUrl = normalizedStoredLoginLogoUrl || DEFAULT_LOGIN_LOGO_URL; return { headerBgColor: settings?.headerBgColor || DEFAULT_HEADER_BG_COLOR, siteName: settings?.siteName || DEFAULT_SITE_NAME, // 管理页编辑的是数据库中的长期地址,预览地址只用于 展示。 headerLogoUrl, loginLogoUrl, headerLogoPreviewUrl: proxyHttpUrl(headerLogoUrl) || headerLogoUrl, loginLogoPreviewUrl: proxyHttpUrl(loginLogoUrl) || loginLogoUrl, maintenanceEnabled: settings?.maintenanceEnabled ?? false, maintenanceMessage: settings?.maintenanceMessage ?? null, maintenanceStartTime: settings?.maintenanceStartTime ?? null, maintenanceEndTime: settings?.maintenanceEndTime ?? null, s3Endpoint: settings?.s3Endpoint || '', s3Bucket: settings?.s3Bucket || '', s3Region: settings?.s3Region || 'us-east-1', s3PublicUrl: settings?.s3PublicUrl || '', s3CredentialsConfigured, brandLogoConfigurationNeedsRepair: Boolean( settings && (!normalizedStoredHeaderLogoUrl || !normalizedStoredLoginLogoUrl), ), }; } async function hasStoredS3Credentials(settingsId: string | null | undefined) { if (!settingsId) { return false; } // 只让数据库判断是否已配置,不把 Secret 原文读入应用进程。 const configuredCount = await prisma.systemSettings.count({ where: { id: settingsId, s3AccessKey: { not: '' }, s3SecretKey: { not: '' }, }, }); return configuredCount > 0; } // GET - 公开读取仅返回展示与维护信息;系统管理员可额外读取非敏感 S3 配置。 export async function GET() { try { const [session, settings] = await Promise.all([ getServerSession(authOptions), prisma.systemSettings.findFirst({ select: systemSettingsSelect }), ]); const user = session?.user as SettingsSessionUser | undefined; const response = canManageSystemSettings(user) ? toAdminSettingsDto( settings, await hasStoredS3Credentials(settings?.id), ) : toPublicSettingsDto(settings); return jsonNoStore(response); } catch (error) { console.error('Get system settings error:', error); return jsonNoStore({ error: '获取系统设置失败' }, 500); } } // PUT - 更新系统设置(仅系统管理员或超级管理员)。 export async function PUT(request: NextRequest) { try { const session = await getServerSession(authOptions); const user = session?.user as SettingsSessionUser | undefined; if (!user?.id) { return jsonNoStore({ error: '未登录' }, 401); } if (!canManageSystemSettings(user)) { return jsonNoStore({ error: '无权限' }, 403); } const { headerBgColor, siteName, headerLogoUrl, loginLogoUrl, s3Endpoint, s3AccessKey, s3SecretKey, s3Bucket, s3Region, s3PublicUrl, maintenanceEnabled, maintenanceMessage, maintenanceStartTime, maintenanceEndTime, } = await request.json(); const nextS3AccessKey = typeof s3AccessKey === 'string' ? s3AccessKey.trim() : ''; const nextS3SecretKey = typeof s3SecretKey === 'string' ? s3SecretKey.trim() : ''; const isCredentialRotationRequested = Boolean( nextS3AccessKey || nextS3SecretKey, ); if ( isCredentialRotationRequested && (!nextS3AccessKey || !nextS3SecretKey) ) { return jsonNoStore( { error: 'Access Key 和 Secret Key 必须同时填写' }, 400, ); } const normalizedHeaderLogoUrl = headerLogoUrl === undefined ? undefined : normalizeBrandLogoUrl(headerLogoUrl); const normalizedLoginLogoUrl = loginLogoUrl === undefined ? undefined : normalizeBrandLogoUrl(loginLogoUrl); if (headerLogoUrl !== undefined && !normalizedHeaderLogoUrl) { return jsonNoStore( { error: '导航栏 LOGO 地址无效,请使用内置品牌资源或当前素材存储桶地址' }, 400, ); } if (loginLogoUrl !== undefined && !normalizedLoginLogoUrl) { return jsonNoStore( { error: '登录页 LOGO 地址无效,请使用内置品牌资源或当前素材存储桶地址' }, 400, ); } const updateData: Prisma.SystemSettingsUpdateInput = {}; if (typeof headerBgColor === 'string') { updateData.headerBgColor = headerBgColor; } if (typeof siteName === 'string') { updateData.siteName = siteName; } if (normalizedHeaderLogoUrl) { updateData.headerLogoUrl = normalizedHeaderLogoUrl; } if (normalizedLoginLogoUrl) { updateData.loginLogoUrl = normalizedLoginLogoUrl; } if (typeof s3Endpoint === 'string') { updateData.s3Endpoint = s3Endpoint; } if (isCredentialRotationRequested) { updateData.s3AccessKey = nextS3AccessKey; updateData.s3SecretKey = nextS3SecretKey; } if (typeof s3Bucket === 'string') { updateData.s3Bucket = s3Bucket; } if (typeof s3Region === 'string') { updateData.s3Region = s3Region; } if (typeof s3PublicUrl === 'string') { updateData.s3PublicUrl = s3PublicUrl; } if (typeof maintenanceEnabled === 'boolean') { updateData.maintenanceEnabled = maintenanceEnabled; } if ( typeof maintenanceMessage === 'string' || maintenanceMessage === null ) { updateData.maintenanceMessage = maintenanceMessage; } if ( typeof maintenanceStartTime === 'string' || maintenanceStartTime === null ) { updateData.maintenanceStartTime = maintenanceStartTime ? new Date(maintenanceStartTime) : null; } if ( typeof maintenanceEndTime === 'string' || maintenanceEndTime === null ) { updateData.maintenanceEndTime = maintenanceEndTime ? new Date(maintenanceEndTime) : null; } const existingSettings = await prisma.systemSettings.findFirst({ select: { id: true }, }); let settings: SystemSettingsRecord; if (existingSettings) { settings = await prisma.systemSettings.update({ where: { id: existingSettings.id }, data: updateData, select: systemSettingsSelect, }); } else { const createData: Prisma.SystemSettingsCreateInput = { headerBgColor: typeof headerBgColor === 'string' && headerBgColor ? headerBgColor : DEFAULT_HEADER_BG_COLOR, headerLogoUrl: normalizedHeaderLogoUrl || DEFAULT_HEADER_LOGO_URL, loginLogoUrl: normalizedLoginLogoUrl || DEFAULT_LOGIN_LOGO_URL, siteName: typeof siteName === 'string' && siteName ? siteName : DEFAULT_SITE_NAME, s3Endpoint: typeof s3Endpoint === 'string' ? s3Endpoint : '', s3AccessKey: isCredentialRotationRequested ? nextS3AccessKey : '', s3SecretKey: isCredentialRotationRequested ? nextS3SecretKey : '', s3Bucket: typeof s3Bucket === 'string' ? s3Bucket : '', s3Region: typeof s3Region === 'string' && s3Region ? s3Region : 'us-east-1', s3PublicUrl: typeof s3PublicUrl === 'string' ? s3PublicUrl : '', maintenanceEnabled: typeof maintenanceEnabled === 'boolean' ? maintenanceEnabled : false, maintenanceMessage: typeof maintenanceMessage === 'string' ? maintenanceMessage : null, maintenanceStartTime: typeof maintenanceStartTime === 'string' && maintenanceStartTime ? new Date(maintenanceStartTime) : null, maintenanceEndTime: typeof maintenanceEndTime === 'string' && maintenanceEndTime ? new Date(maintenanceEndTime) : null, }; settings = await prisma.systemSettings.create({ data: createData, select: systemSettingsSelect, }); } const s3CredentialsConfigured = isCredentialRotationRequested ? true : await hasStoredS3Credentials(settings.id); return jsonNoStore( toAdminSettingsDto(settings, s3CredentialsConfigured), ); } catch (error) { console.error('Update system settings error:', error); return jsonNoStore({ error: '更新系统设置失败' }, 500); } }