localgreenchain/pages/api/upload/presigned.ts
Claude d74128d3cd
Add Agent 3: File Upload & Storage System
Implements cloud-based file storage for plant photos, documents, and certificates:

Storage Layer:
- Multi-provider support (AWS S3, Cloudflare R2, MinIO, local filesystem)
- S3-compatible provider with presigned URL generation
- Local storage provider for development with signed URL verification
- Configurable via environment variables

Image Processing:
- Automatic thumbnail generation (150x150, 300x300, 600x600, 1200x1200)
- WebP conversion for optimized file sizes
- EXIF data extraction for image metadata
- Image optimization with Sharp

API Endpoints:
- POST /api/upload/image - Upload images with automatic processing
- POST /api/upload/document - Upload documents (PDF, DOC, DOCX)
- POST /api/upload/presigned - Get presigned URLs for direct uploads
- GET/DELETE /api/upload/[fileId] - File management

UI Components:
- ImageUploader - Drag & drop image upload with preview
- PhotoGallery - Grid gallery with lightbox view
- DocumentUploader - Document upload with file type icons
- ProgressBar - Animated upload progress indicator

Database:
- FileStore service with in-memory storage (Prisma schema ready for Agent 2)
- File metadata tracking with soft delete support
- Category-based file organization
2025-11-23 03:51:31 +00:00

92 lines
2.5 KiB
TypeScript

/**
* Presigned URL API Endpoint
* Agent 3: File Upload & Storage System
*
* POST /api/upload/presigned - Get a presigned URL for direct upload
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getUploadService } from '../../../lib/storage';
import type { FileCategory, PresignedUrlResponse } from '../../../lib/storage/types';
interface RequestBody {
filename: string;
contentType: string;
category: FileCategory;
expiresIn?: number;
}
interface PresignedResponse {
success: boolean;
data?: PresignedUrlResponse;
error?: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<PresignedResponse>
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { filename, contentType, category, expiresIn } = req.body as RequestBody;
// Validate required fields
if (!filename || !contentType || !category) {
return res.status(400).json({
success: false,
error: 'Missing required fields: filename, contentType, category',
});
}
// Validate category
const validCategories: FileCategory[] = ['plant-photo', 'certificate', 'document', 'report', 'avatar'];
if (!validCategories.includes(category)) {
return res.status(400).json({
success: false,
error: `Invalid category. Must be one of: ${validCategories.join(', ')}`,
});
}
// Validate content type
const allowedContentTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/heic',
'image/heif',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
];
if (!allowedContentTypes.includes(contentType)) {
return res.status(400).json({
success: false,
error: `Invalid content type. Must be one of: ${allowedContentTypes.join(', ')}`,
});
}
const uploadService = getUploadService();
const presignedUrl = await uploadService.getPresignedUploadUrl({
filename,
contentType,
category,
expiresIn: expiresIn || 3600, // Default 1 hour
});
return res.status(200).json({
success: true,
data: presignedUrl,
});
} catch (error) {
console.error('Presigned URL error:', error);
return res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Internal server error',
});
}
}