import { PAGE_PASSWORD_AUTH_ERROR_CODES } from '@/lib/page-password-auth'; import { verifyAccessToken } from '@/lib/server-runtime'; import { createImageShare } from '@/lib/share-store'; import { NextRequest, NextResponse } from 'next/server'; const MAX_SHARE_IMAGE_BYTES = 30 * 1024 * 1024; const MIN_ACCESS_CODE_LENGTH = 8; const MAX_ACCESS_CODE_LENGTH = 128; const MAX_SOURCE_FILENAME_LENGTH = 200; const SOURCE_FILENAME_PATTERN = /^[^\x00-\x1f\x7f\\/]+$/u; function detectImageMimeType(buffer: Buffer): string | undefined { if ( buffer.length >= 24 && buffer[0] === 0x89 && buffer.toString('ascii', 1, 4) === 'PNG' && buffer.toString('ascii', 12, 16) === 'IHDR' ) { return 'image/png'; } if (buffer.length >= 4 && buffer[0] === 0xff && buffer[1] === 0xd8) { return 'image/jpeg'; } if ( buffer.length >= 30 && buffer.toString('ascii', 0, 4) === 'RIFF' && buffer.toString('ascii', 8, 12) === 'WEBP' ) { return 'image/webp'; } return undefined; } function jsonError(code: string, message: string, status: number) { return NextResponse.json({ error: message, code }, { status }); } function parseExpiry(value: FormDataEntryValue | null): number | null | undefined { if (value === null || value === '') return null; if (typeof value !== 'string' || !/^\d+$/.test(value)) return undefined; const minutes = Number(value); if (!Number.isSafeInteger(minutes) || minutes < 1 || minutes > 60 * 24 * 30) return undefined; return minutes; } function resolveShareUrl(request: Request, token: string): string { const pathname = new URL(request.url).pathname.replace(/\/+$/, ''); const apiPath = '/api/shares'; const basePath = pathname.endsWith(apiPath) ? pathname.slice(0, -apiPath.length) : ''; return `${basePath}/share/${encodeURIComponent(token)}`; } function isUploadedImage(value: FormDataEntryValue | null): value is File { return ( typeof value === 'object' && value !== null && typeof (value as Blob).arrayBuffer === 'function' && typeof (value as Blob).size === 'number' ); } function parseAccessCode(value: FormDataEntryValue | null): string | undefined | null { if (value === null) return undefined; if (typeof value !== 'string') return null; const trimmed = value.trim(); if (!trimmed) return undefined; if (trimmed.length < MIN_ACCESS_CODE_LENGTH || trimmed.length > MAX_ACCESS_CODE_LENGTH) return null; return trimmed; } function parseSourceFilename(value: FormDataEntryValue | null, fallback: string): string | null { if (value !== null && typeof value !== 'string') return null; const candidate = value?.trim() || fallback.trim(); if (!candidate || candidate.length > MAX_SOURCE_FILENAME_LENGTH || !SOURCE_FILENAME_PATTERN.test(candidate)) { return null; } return candidate; } function verifyShareCreator(request: NextRequest) { const appPassword = process.env.APP_PASSWORD?.trim(); if (!appPassword) return undefined; const accessToken = request.cookies.get('gptImageAccess')?.value; if (verifyAccessToken(accessToken, appPassword)) return undefined; const code = accessToken ? PAGE_PASSWORD_AUTH_ERROR_CODES.invalid : PAGE_PASSWORD_AUTH_ERROR_CODES.missing; return jsonError(code, '未授权:无效的访问令牌。', 401); } export async function POST(request: NextRequest) { const authError = verifyShareCreator(request); if (authError) return authError; let form: FormData; try { form = await request.formData(); } catch { return jsonError('invalid_form_data', '分享请求格式无效。', 400); } const image = form.get('image'); if (!isUploadedImage(image)) { return jsonError('image_required', '分享图片必填。', 400); } if (image.size <= 0 || image.size > MAX_SHARE_IMAGE_BYTES) { return jsonError('invalid_image_size', '分享图片大小无效。', 400); } const sourceFilenameValue = form.get('sourceFilename'); const fallbackFilename = typeof image.name === 'string' && image.name.trim() ? image.name : 'shared-image.png'; const sourceFilename = parseSourceFilename(sourceFilenameValue, fallbackFilename); if (sourceFilename === null) { return jsonError('invalid_source_filename', '分享文件名无效。', 400); } const expiresInMinutes = parseExpiry(form.get('expiresInMinutes')); if (expiresInMinutes === undefined) { return jsonError('invalid_expiry', '分享有效期无效。', 400); } const accessCode = parseAccessCode(form.get('accessCode')); if (accessCode === null) { return jsonError('invalid_access_code', '访问码长度无效。', 400); } const imageBuffer = Buffer.from(await image.arrayBuffer()); const detectedMimeType = detectImageMimeType(imageBuffer); if (!detectedMimeType) { return jsonError('invalid_image_content', '分享图片内容不是有效的 PNG、JPEG 或 WebP 文件。', 400); } const record = await createImageShare({ imageBuffer, sourceFilename, mimeType: detectedMimeType, accessCode, expiresInMinutes }); return NextResponse.json( { token: record.token, url: resolveShareUrl(request, record.token), expiresAt: record.expiresAt ?? null, accessCodeRequired: record.accessCodeRequired }, { status: 201 } ); }