monacousa-portal/server/api/profile/upload-image.post.ts

146 lines
4.3 KiB
TypeScript

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',
});
}
});