import { put, del } from '../../../../lib/object-storage.js'; import { sql } from '../../../../lib/sql.js'; import { getUserFromRequest } from '../../../../lib/permission-middleware'; import { checkGenerateRateLimit } from '../../../../lib/rate-limit.js'; export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { // Get authenticated user const user = await getUserFromRequest(req); if (!user) { return res.status(401).json({ error: 'Authentication required' }); } const { allowed, reset } = await checkGenerateRateLimit(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.' }); } // Get user information for avatar generation const userResult = await sql` SELECT email, first_name, last_name, username FROM users WHERE id = ${user.userId} `; if (userResult.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } const userData = userResult.rows[0]; try { // Delete old avatar if exists await deleteOldAvatar(user.userId); // Generate avatar using a service (we'll use DiceBear Avatars as an example) const avatarStyle = 'initials'; // You can change this to other styles like 'avataaars', 'bottts', etc. const seed = userData.username || userData.email || `user-${user.userId}`; const initials = getInitials(userData); // Create avatar URL with DiceBear API const avatarUrl = `https://api.dicebear.com/7.x/${avatarStyle}/svg?seed=${encodeURIComponent(seed)}&chars=2&backgroundColor=d84315,ff5722,ff7043&textColor=ffffff&fontSize=40`; // Fetch the generated avatar const avatarResponse = await fetch(avatarUrl); if (!avatarResponse.ok) { throw new Error('Failed to generate avatar'); } const avatarBuffer = Buffer.from(await avatarResponse.arrayBuffer()); // Generate unique filename const filename = `avatars/generated-${user.userId}-${Date.now()}.svg`; // Upload to MinIO const blob = await put(filename, avatarBuffer, { contentType: 'image/svg+xml', }); // 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}, 'generated-avatar.svg', 'image/svg+xml', ${avatarBuffer.length}, ${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 generated successfully', avatar_url: blob.url }); } catch (generateError) { console.error('Avatar generation error:', generateError); res.status(500).json({ error: 'Failed to generate avatar' }); } } catch (error) { console.error('Avatar generation API error:', error); res.status(500).json({ error: 'Internal server error' }); } } /** * Get user initials for avatar generation */ function getInitials(userData) { if (userData.first_name || userData.last_name) { return `${userData.first_name?.charAt(0) || ''}${userData.last_name?.charAt(0) || ''}`.toUpperCase(); } if (userData.username) { return userData.username.substring(0, 2).toUpperCase(); } return userData.email?.charAt(0).toUpperCase() || 'U'; } /** * Delete old avatar from object storage 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]; try { await del(avatar.file_path); } catch (blobError) { console.warn('Failed to delete object storage 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 } }