deckhearth/pages/api/user/avatar.js

216 lines
6.6 KiB
JavaScript
Raw Permalink Normal View History

🖼️ Complete Avatar Upload System with Vercel Blob 📤 Avatar Upload API (/api/user/avatar): - File upload with multipart form data parsing - Comprehensive validation (file type, size limits) - Support for JPEG, PNG, GIF, WebP images up to 5MB - Automatic cleanup of old avatars before new uploads - Vercel Blob integration with public access - Database tracking in user_avatars table - Error handling for upload failures 🎨 Avatar Generation API (/api/user/avatar/generate): - Custom avatar generation using DiceBear API - Fire-themed color scheme (matching app branding) - Personalized based on user initials/username/email - SVG format for crisp display at any size - Automatic fallback if generation fails - Same cleanup and storage workflow as uploads 🗑️ Account Deletion API (/api/user/delete): - Complete user data cleanup including Vercel Blob files - Cascading deletion respecting foreign key constraints - Admin account protection (prevents self-deletion) - Comprehensive cleanup order: * User avatars from Vercel Blob storage * Deck cards, decks, collection cards, collections * User cards, avatar records, settings * Finally the user account itself - Detailed logging for audit trail - Graceful error handling with specific error messages 🔧 Technical Features: - Custom multipart form data parser for file uploads - Vercel Blob put/del operations with error handling - Unique filename generation with timestamps - Database transaction-like cleanup for deletions - File type validation and size limits - Proper CORS headers for all endpoints 🎯 Integration Ready: - Works seamlessly with existing profile page UI - Supports both upload and generate avatar buttons - Returns avatar URLs for immediate display - Database consistency with user profile system - Production-ready error handling and validation The avatar system is now fully functional with Vercel Blob! 📸✨
2025-07-26 22:39:42 -04:00
import { put, del } from '@vercel/blob';
import { sql } from '@vercel/postgres';
import { getUserFromRequest } from '../../../lib/permission-middleware';
import { checkUploadRateLimit } from '../../../lib/rate-limit.js';
🖼️ Complete Avatar Upload System with Vercel Blob 📤 Avatar Upload API (/api/user/avatar): - File upload with multipart form data parsing - Comprehensive validation (file type, size limits) - Support for JPEG, PNG, GIF, WebP images up to 5MB - Automatic cleanup of old avatars before new uploads - Vercel Blob integration with public access - Database tracking in user_avatars table - Error handling for upload failures 🎨 Avatar Generation API (/api/user/avatar/generate): - Custom avatar generation using DiceBear API - Fire-themed color scheme (matching app branding) - Personalized based on user initials/username/email - SVG format for crisp display at any size - Automatic fallback if generation fails - Same cleanup and storage workflow as uploads 🗑️ Account Deletion API (/api/user/delete): - Complete user data cleanup including Vercel Blob files - Cascading deletion respecting foreign key constraints - Admin account protection (prevents self-deletion) - Comprehensive cleanup order: * User avatars from Vercel Blob storage * Deck cards, decks, collection cards, collections * User cards, avatar records, settings * Finally the user account itself - Detailed logging for audit trail - Graceful error handling with specific error messages 🔧 Technical Features: - Custom multipart form data parser for file uploads - Vercel Blob put/del operations with error handling - Unique filename generation with timestamps - Database transaction-like cleanup for deletions - File type validation and size limits - Proper CORS headers for all endpoints 🎯 Integration Ready: - Works seamlessly with existing profile page UI - Supports both upload and generate avatar buttons - Returns avatar URLs for immediate display - Database consistency with user profile system - Production-ready error handling and validation The avatar system is now fully functional with Vercel Blob! 📸✨
2025-07-26 22:39:42 -04:00
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' });
}
const { allowed, reset } = await checkUploadRateLimit(req, user.userId);
if (!allowed) {
res.setHeader('Retry-After', Math.ceil((reset - Date.now()) / 1000));
return res.status(429).json({ error: 'Too many attempts. Try again later.' });
}
🖼️ Complete Avatar Upload System with Vercel Blob 📤 Avatar Upload API (/api/user/avatar): - File upload with multipart form data parsing - Comprehensive validation (file type, size limits) - Support for JPEG, PNG, GIF, WebP images up to 5MB - Automatic cleanup of old avatars before new uploads - Vercel Blob integration with public access - Database tracking in user_avatars table - Error handling for upload failures 🎨 Avatar Generation API (/api/user/avatar/generate): - Custom avatar generation using DiceBear API - Fire-themed color scheme (matching app branding) - Personalized based on user initials/username/email - SVG format for crisp display at any size - Automatic fallback if generation fails - Same cleanup and storage workflow as uploads 🗑️ Account Deletion API (/api/user/delete): - Complete user data cleanup including Vercel Blob files - Cascading deletion respecting foreign key constraints - Admin account protection (prevents self-deletion) - Comprehensive cleanup order: * User avatars from Vercel Blob storage * Deck cards, decks, collection cards, collections * User cards, avatar records, settings * Finally the user account itself - Detailed logging for audit trail - Graceful error handling with specific error messages 🔧 Technical Features: - Custom multipart form data parser for file uploads - Vercel Blob put/del operations with error handling - Unique filename generation with timestamps - Database transaction-like cleanup for deletions - File type validation and size limits - Proper CORS headers for all endpoints 🎯 Integration Ready: - Works seamlessly with existing profile page UI - Supports both upload and generate avatar buttons - Returns avatar URLs for immediate display - Database consistency with user profile system - Production-ready error handling and validation The avatar system is now fully functional with Vercel Blob! 📸✨
2025-07-26 22:39:42 -04:00
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
}
}