import { put, del } from '@vercel/blob'; import { sql } from '@vercel/postgres'; import { getUserFromRequest } from '../../../lib/permission-middleware'; export const config = { api: { bodyParser: { sizeLimit: '5mb', }, }, }; export default async function handler(req, res) { try { // Get authenticated user const user = await getUserFromRequest(req); if (!user) { return res.status(401).json({ error: 'Authentication required' }); } if (req.method === 'POST') { // Handle avatar upload const contentType = req.headers['content-type']; if (!contentType || !contentType.startsWith('multipart/form-data')) { return res.status(400).json({ error: 'Content-Type must be multipart/form-data' }); } // Parse multipart form data const formData = await parseMultipartFormData(req); const file = formData.avatar; if (!file) { return res.status(400).json({ error: 'No avatar file provided' }); } // Validate file type const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; if (!allowedTypes.includes(file.type)) { return res.status(400).json({ error: 'Invalid file type. Please upload a JPEG, PNG, GIF, or WebP image.' }); } // Validate file size (5MB limit) if (file.size > 5 * 1024 * 1024) { return res.status(400).json({ error: 'File size must be less than 5MB' }); } try { // Delete old avatar if exists await deleteOldAvatar(user.userId); // Generate unique filename const fileExtension = file.type.split('/')[1]; const filename = `avatars/${user.userId}-${Date.now()}.${fileExtension}`; // Upload to Vercel Blob const blob = await put(filename, file.buffer, { access: 'public', contentType: file.type, }); // Save avatar info to database await sql` INSERT INTO user_avatars (user_id, filename, original_name, mime_type, file_size, file_path, is_active) VALUES (${user.userId}, ${filename}, ${file.originalName}, ${file.type}, ${file.size}, ${blob.url}, true) `; // Update user's avatar_url await sql` UPDATE users SET avatar_url = ${blob.url}, updated_at = CURRENT_TIMESTAMP WHERE id = ${user.userId} `; res.status(200).json({ message: 'Avatar uploaded successfully', avatar_url: blob.url }); } catch (uploadError) { console.error('Avatar upload error:', uploadError); res.status(500).json({ error: 'Failed to upload avatar' }); } } else if (req.method === 'DELETE') { // Handle avatar deletion try { await deleteOldAvatar(user.userId); // Clear user's avatar_url await sql` UPDATE users SET avatar_url = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ${user.userId} `; res.status(200).json({ message: 'Avatar deleted successfully' }); } catch (deleteError) { console.error('Avatar deletion error:', deleteError); res.status(500).json({ error: 'Failed to delete avatar' }); } } else { res.status(405).json({ error: 'Method not allowed' }); } } catch (error) { console.error('Avatar API error:', error); res.status(500).json({ error: 'Internal server error' }); } } /** * Parse multipart form data manually */ async function parseMultipartFormData(req) { return new Promise((resolve, reject) => { const chunks = []; req.on('data', (chunk) => { chunks.push(chunk); }); req.on('end', () => { try { const buffer = Buffer.concat(chunks); const boundary = req.headers['content-type'].split('boundary=')[1]; const parts = buffer.toString('binary').split(`--${boundary}`); const formData = {}; for (const part of parts) { if (part.includes('Content-Disposition: form-data')) { const nameMatch = part.match(/name="([^"]+)"/); const filenameMatch = part.match(/filename="([^"]+)"/); const contentTypeMatch = part.match(/Content-Type: ([^\r\n]+)/); if (nameMatch) { const fieldName = nameMatch[1]; const headerEndIndex = part.indexOf('\r\n\r\n'); if (headerEndIndex !== -1) { const content = part.substring(headerEndIndex + 4); const contentBuffer = Buffer.from(content, 'binary'); if (filenameMatch && contentTypeMatch) { // This is a file field formData[fieldName] = { originalName: filenameMatch[1], type: contentTypeMatch[1], buffer: contentBuffer.slice(0, -2), // Remove trailing \r\n size: contentBuffer.length - 2 }; } else { // This is a regular field formData[fieldName] = content.trim(); } } } } } resolve(formData); } catch (error) { reject(error); } }); req.on('error', reject); }); } /** * Delete old avatar from Vercel Blob and database */ async function deleteOldAvatar(userId) { try { // Get current active avatar const avatarResult = await sql` SELECT file_path, filename FROM user_avatars WHERE user_id = ${userId} AND is_active = true `; if (avatarResult.rows.length > 0) { const avatar = avatarResult.rows[0]; // Delete from Vercel Blob try { await del(avatar.file_path); } catch (blobError) { console.warn('Failed to delete blob file:', blobError); // Continue anyway - the database record should still be cleaned up } // Mark as inactive in database await sql` UPDATE user_avatars SET is_active = false, updated_at = CURRENT_TIMESTAMP WHERE user_id = ${userId} AND is_active = true `; } } catch (error) { console.error('Error deleting old avatar:', error); // Don't throw - this shouldn't prevent new uploads } }