import formidable from 'formidable'; import { promises as fs } from 'fs'; import { uploadProfileImage, updateMemberProfileImageUrl, validateImageFile } from '~/server/utils/profile-images'; import { createSessionManager } from '~/server/utils/session'; // Role-based access control using consistent session structure function canEditMember(user: any, targetMemberId: string): boolean { // Admin can edit anyone if (user.tier === 'admin') { return true; } // Board members can edit anyone if (user.tier === 'board') { return true; } // Users can only edit their own profile // Match by email, member_id, or user ID return user.email === targetMemberId || user.member_id === targetMemberId || user.id === targetMemberId; } export default defineEventHandler(async (event) => { console.log('[profile-upload] ========================='); console.log('[profile-upload] POST /api/profile/upload-image'); console.log('[profile-upload] Request from:', getClientIP(event)); try { // Get user session using the working session manager const sessionManager = createSessionManager(); const cookieHeader = getHeader(event, 'cookie'); const session = sessionManager.getSession(cookieHeader); if (!session || !session.user) { console.log('[profile-upload] ❌ No valid session found'); throw createError({ statusCode: 401, statusMessage: 'Authentication required' }); } console.log('[profile-upload] ✅ Valid session found for user:', session.user.email); console.log('[profile-upload] User tier:', session.user.tier); // Get query parameters const query = getQuery(event); const targetMemberId = query.memberId as string; if (!targetMemberId) { throw createError({ statusCode: 400, statusMessage: 'Member ID is required', }); } // Check permissions if (!canEditMember(session.user, targetMemberId)) { console.log('[profile-upload] ❌ Permission denied for user:', session.user.email, 'target:', targetMemberId); throw createError({ statusCode: 403, statusMessage: 'You can only upload images for your own profile', }); } console.log(`[profile-upload] Processing upload for member: ${targetMemberId}`); // Parse multipart form data const form = formidable({ maxFileSize: 5 * 1024 * 1024, // 5MB limit keepExtensions: true, allowEmptyFiles: false, maxFiles: 1, }); const [fields, files] = await form.parse(event.node.req); // Get the uploaded file const uploadedFile = Array.isArray(files.image) ? files.image[0] : files.image; if (!uploadedFile) { throw createError({ statusCode: 400, statusMessage: 'No image file provided', }); } console.log(`[profile-upload] File received: ${uploadedFile.originalFilename}, size: ${uploadedFile.size} bytes`); // Read file buffer const fileBuffer = await fs.readFile(uploadedFile.filepath); // Validate the image file validateImageFile(fileBuffer, uploadedFile.originalFilename || 'image.jpg'); // Upload image and generate thumbnails const imagePath = await uploadProfileImage( targetMemberId, fileBuffer, uploadedFile.originalFilename || 'profile.jpg' ); // Update database with image path await updateMemberProfileImageUrl( targetMemberId, imagePath, uploadedFile.originalFilename || 'profile.jpg' ); // Clean up temporary file try { await fs.unlink(uploadedFile.filepath); } catch (error) { console.warn('[profile-upload] Failed to clean up temp file:', error); } console.log(`[profile-upload] Successfully uploaded profile image for member: ${targetMemberId}`); return { success: true, message: 'Profile image uploaded successfully', imagePath, originalName: uploadedFile.originalFilename, size: uploadedFile.size, }; } catch (error: any) { console.error('[profile-upload] Upload failed:', error); // Provide specific error messages if (error.statusCode) { throw error; // Re-throw HTTP errors } throw createError({ statusCode: 500, statusMessage: error.message || 'Profile image upload failed', }); } });