visual-journal / src /app /api /shares /route.ts
gpt-image-playground deploy
Deploy 912aa68 to Docker Space
704e84f
Raw
History Blame Contribute Delete
5.57 kB
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 }
);
}