2026-07-31 09:41:40 +08:00

304 lines
9.2 KiB
TypeScript

// 系统设置 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';
const DEFAULT_HEADER_BG_COLOR = '#1f2937';
const DEFAULT_SITE_NAME = '由壹点3D素材';
const DEFAULT_LOGO_URL =
'https://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png';
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 = settings?.headerLogoUrl || DEFAULT_LOGO_URL;
const loginLogoUrl = settings?.loginLogoUrl || DEFAULT_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,
) {
return {
...toPublicSettingsDto(settings),
s3Endpoint: settings?.s3Endpoint || '',
s3Bucket: settings?.s3Bucket || '',
s3Region: settings?.s3Region || 'us-east-1',
s3PublicUrl: settings?.s3PublicUrl || '',
s3CredentialsConfigured,
};
}
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 updateData: Prisma.SystemSettingsUpdateInput = {};
if (typeof headerBgColor === 'string') {
updateData.headerBgColor = headerBgColor;
}
if (typeof siteName === 'string') {
updateData.siteName = siteName;
}
if (typeof headerLogoUrl === 'string') {
updateData.headerLogoUrl = headerLogoUrl;
}
if (typeof loginLogoUrl === 'string') {
updateData.loginLogoUrl = loginLogoUrl;
}
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:
typeof headerLogoUrl === 'string' && headerLogoUrl
? headerLogoUrl
: DEFAULT_LOGO_URL,
loginLogoUrl:
typeof loginLogoUrl === 'string' && loginLogoUrl
? loginLogoUrl
: DEFAULT_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);
}
}