109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { db, media } from '@/db';
|
|
import { requireAuth } from '@/lib/auth';
|
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB for images
|
|
const MAX_VIDEO_SIZE = 100 * 1024 * 1024; // 100MB for videos
|
|
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime'];
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const user = await requireAuth();
|
|
|
|
const formData = await req.formData();
|
|
const file = formData.get('file') as File | null;
|
|
const altText = (formData.get('alt') as string | null) || null;
|
|
|
|
if (!file) {
|
|
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
|
}
|
|
|
|
// Validate file type
|
|
const isImage = ALLOWED_IMAGE_TYPES.includes(file.type);
|
|
const isVideo = ALLOWED_VIDEO_TYPES.includes(file.type);
|
|
|
|
if (!isImage && !isVideo) {
|
|
return NextResponse.json({
|
|
error: 'Invalid file type. Allowed: JPEG, PNG, GIF, WebP, MP4, WebM, MOV'
|
|
}, { status: 400 });
|
|
}
|
|
|
|
// Validate file size based on type
|
|
const maxSize = isVideo ? MAX_VIDEO_SIZE : MAX_IMAGE_SIZE;
|
|
if (file.size > maxSize) {
|
|
return NextResponse.json({
|
|
error: `File too large. Maximum size: ${isVideo ? '100MB' : '10MB'}`
|
|
}, { status: 400 });
|
|
}
|
|
|
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
// Sanitize filename to be safe for S3 keys
|
|
const filename = `${uuidv4()}-${file.name.replace(/[^a-zA-Z0-9.-]/g, '')}`;
|
|
|
|
// S3 Configuration
|
|
const s3 = new S3Client({
|
|
region: process.env.STORAGE_REGION || 'us-east-1',
|
|
endpoint: process.env.STORAGE_ENDPOINT,
|
|
credentials: {
|
|
accessKeyId: process.env.STORAGE_ACCESS_KEY || '',
|
|
secretAccessKey: process.env.STORAGE_SECRET_KEY || '',
|
|
},
|
|
forcePathStyle: true, // Needed for many S3-compatible providers
|
|
});
|
|
|
|
const bucket = process.env.STORAGE_BUCKET || 'synapsis';
|
|
|
|
await s3.send(new PutObjectCommand({
|
|
Bucket: bucket,
|
|
Key: filename,
|
|
Body: buffer,
|
|
ContentType: file.type,
|
|
ACL: 'public-read',
|
|
}));
|
|
|
|
// Construct Public URL
|
|
let url = '';
|
|
if (process.env.STORAGE_PUBLIC_BASE_URL) {
|
|
url = `${process.env.STORAGE_PUBLIC_BASE_URL}/${filename}`;
|
|
} else if (process.env.STORAGE_ENDPOINT) {
|
|
url = `${process.env.STORAGE_ENDPOINT}/${bucket}/${filename}`;
|
|
} else {
|
|
return NextResponse.json({ error: 'Storage not configured - missing STORAGE_PUBLIC_BASE_URL or STORAGE_ENDPOINT' }, { status: 500 });
|
|
}
|
|
|
|
// Store media record
|
|
if (db) {
|
|
const [mediaRecord] = await db.insert(media).values({
|
|
userId: user.id,
|
|
postId: null,
|
|
url,
|
|
altText,
|
|
mimeType: file.type,
|
|
width: 0, // TODO: Get actual dimensions
|
|
height: 0,
|
|
}).returning();
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
media: mediaRecord,
|
|
url,
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
url,
|
|
});
|
|
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message === 'Authentication required') {
|
|
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
|
|
}
|
|
console.error('Upload error:', error);
|
|
return NextResponse.json({ error: 'Upload failed' }, { status: 500 });
|
|
}
|
|
}
|