// 上传系统Logo API import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; import { prisma } from '@/lib/prisma'; import { uploadToS3, verifyUploadedS3Object } from '@/lib/s3'; import { hasSystemAdminPermission } from '@/lib/department-permission'; import { DEFAULT_HEADER_LOGO_URL, DEFAULT_LOGIN_LOGO_URL, normalizeBrandLogoUrl, } from '@/lib/brand-assets'; import { MAX_BRAND_IMAGE_BYTES, validateBrandImage } from '@/lib/brand-image-validation'; import { assertBoundedMultipartHeaders, assertMultipartFiles } from '@/lib/multipart-request-guard'; import { boundedMultipartRequestLimiter, getBoundedMultipartFailure, MULTIPART_ROUTE_LIMITS, } from '@/lib/multipart-route-limits'; // POST - 上传Logo(仅管理员) export async function POST(request: NextRequest) { let releaseRequestSlot: (() => void) | null = null; try { const session = await getServerSession(authOptions); if (!session?.user || !(session.user as any).id) { return NextResponse.json( { error: '未登录' }, { status: 401 } ); } const sessionUser = session.user as any; if (!hasSystemAdminPermission(sessionUser.role, Boolean(sessionUser.isSuperAdmin))) { return NextResponse.json( { error: '无权限' }, { status: 403 } ); } assertBoundedMultipartHeaders(request.headers, MULTIPART_ROUTE_LIMITS.logoRequestBytes); releaseRequestSlot = await boundedMultipartRequestLimiter.acquire(); const formData = await request.formData(); assertMultipartFiles(formData, { fields: { logo: { maxCount: 1, maxFileBytes: MULTIPART_ROUTE_LIMITS.logoFileBytes, allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'], }, }, maxFiles: 1, maxTotalBytes: MULTIPART_ROUTE_LIMITS.logoFileBytes, }); const logo = formData.get('logo') as File | null; const type = formData.get('type') as string; if (!logo || typeof logo.arrayBuffer !== 'function') { return NextResponse.json( { error: '请选择LOGO文件' }, { status: 400 } ); } if (!type || !['header', 'login'].includes(type)) { return NextResponse.json( { error: '请指定LOGO类型(header或login)' }, { status: 400 } ); } if (!Number.isFinite(logo.size) || logo.size <= 0 || logo.size > MAX_BRAND_IMAGE_BYTES) { return NextResponse.json( { error: 'Logo 必须是 5MB 以内的有效图片' }, { status: 400 } ); } if (!['image/png', 'image/jpeg', 'image/webp'].includes(logo.type.toLowerCase())) { return NextResponse.json( { error: '只支持 PNG、JPEG 或 WebP 图片' }, { status: 400 } ); } // 转换为Buffer const arrayBuffer = await logo.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); if (buffer.length !== logo.size || buffer.length > MAX_BRAND_IMAGE_BYTES) { return NextResponse.json( { error: 'Logo 文件大小校验失败' }, { status: 400 } ); } let detected try { detected = await validateBrandImage(buffer, logo.type) } catch { return NextResponse.json( { error: 'Logo 文件损坏、格式不一致或尺寸过大' }, { status: 400 } ); } // 生成文件名 const timestamp = Date.now(); const filename = `${type}-logo-${timestamp}.${detected.extension}`; // 上传到S3 const uploadedLogoUrl = await uploadToS3(buffer, filename, detected.contentType); const logoUrl = normalizeBrandLogoUrl(uploadedLogoUrl) if (!logoUrl) { throw new Error('对象存储返回了不受信任的品牌图片地址') } await verifyUploadedS3Object(logoUrl, buffer.length, detected.contentType) // 更新数据库 const existingSettings = await prisma.systemSettings.findFirst({ select: { id: true }, }); const fieldName = type === 'header' ? 'headerLogoUrl' : 'loginLogoUrl'; if (existingSettings) { await prisma.systemSettings.update({ where: { id: existingSettings.id }, data: { [fieldName]: logoUrl }, select: { id: true }, }); } else { const initialLogos = { headerLogoUrl: DEFAULT_HEADER_LOGO_URL, loginLogoUrl: DEFAULT_LOGIN_LOGO_URL, [fieldName]: logoUrl, } await prisma.systemSettings.create({ data: initialLogos, select: { id: true }, }); } return NextResponse.json({ success: true, logoUrl, type, }); } catch (error) { console.error('Upload logo error:', error); const boundedFailure = getBoundedMultipartFailure(error); if (boundedFailure) { return NextResponse.json( { error: boundedFailure.message, code: boundedFailure.code }, { status: boundedFailure.status, ...(boundedFailure.retryAfterSeconds ? { headers: { 'Retry-After': String(boundedFailure.retryAfterSeconds) } } : {}), }, ); } return NextResponse.json( { error: '上传LOGO失败' }, { status: 500 } ); } finally { releaseRequestSlot?.(); } }