import { updateMember, handleNocoDbError } from '~/server/utils/nocodb'; import { createSessionManager } from '~/server/utils/session'; import type { Member, MembershipStatus } from '~/utils/types'; export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id'); console.log('[api/members/[id].put] ========================='); console.log('[api/members/[id].put] PUT /api/members/' + id); console.log('[api/members/[id].put] Request from:', getClientIP(event)); if (!id) { throw createError({ statusCode: 400, statusMessage: 'Member ID is required' }); } try { // Validate session and require Board+ privileges const sessionManager = createSessionManager(); const cookieHeader = getCookie(event, 'monacousa-session') ? getHeader(event, 'cookie') : undefined; const session = sessionManager.getSession(cookieHeader); if (!session?.user) { throw createError({ statusCode: 401, statusMessage: 'Authentication required' }); } const userTier = session.user.tier; if (userTier !== 'board' && userTier !== 'admin') { throw createError({ statusCode: 403, statusMessage: 'Board member privileges required to update members' }); } console.log('[api/members/[id].put] Authorized user:', session.user.email, 'Tier:', userTier); // Get and validate request body const body = await readBody(event); console.log('[api/members/[id].put] Request body fields:', Object.keys(body)); // Validate updated fields const validationErrors = validateMemberUpdateData(body); if (validationErrors.length > 0) { console.error('[api/members/[id].put] Validation errors:', validationErrors); throw createError({ statusCode: 400, statusMessage: `Validation failed: ${validationErrors.join(', ')}` }); } // Sanitize and prepare data const memberData = sanitizeMemberUpdateData(body); console.log('[api/members/[id].put] Sanitized data fields:', Object.keys(memberData)); // Update member in NocoDB const updatedMember = await updateMember(id, memberData); console.log('[api/members/[id].put] ✅ Member updated successfully:', id); // Return processed member const processedMember = { ...updatedMember, FullName: `${updatedMember['First Name'] || ''} ${updatedMember['Last Name'] || ''}`.trim(), FormattedPhone: formatPhoneNumber(updatedMember.Phone) }; return { success: true, data: processedMember, message: 'Member updated successfully' }; } catch (error: any) { console.error('[api/members/[id].put] ❌ Error updating member:', error); handleNocoDbError(error, 'updateMember', 'Member'); } }); function validateMemberUpdateData(data: any): string[] { const errors: string[] = []; // Only validate fields that are provided (partial updates allowed) if (data['First Name'] !== undefined) { if (!data['First Name'] || typeof data['First Name'] !== 'string' || data['First Name'].trim().length < 2) { errors.push('First Name must be at least 2 characters'); } } if (data['Last Name'] !== undefined) { if (!data['Last Name'] || typeof data['Last Name'] !== 'string' || data['Last Name'].trim().length < 2) { errors.push('Last Name must be at least 2 characters'); } } if (data.Email !== undefined) { if (!data.Email || typeof data.Email !== 'string' || !isValidEmail(data.Email)) { errors.push('Valid email address is required'); } } // Optional field validation if (data.Phone !== undefined && data.Phone && typeof data.Phone === 'string' && data.Phone.trim()) { const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/; const cleanPhone = data.Phone.replace(/\D/g, ''); if (!phoneRegex.test(cleanPhone)) { errors.push('Phone number format is invalid'); } } if (data['Membership Status'] !== undefined && !['Active', 'Inactive', 'Pending', 'Expired'].includes(data['Membership Status'])) { errors.push('Invalid membership status'); } return errors; } function sanitizeMemberUpdateData(data: any): Partial { const sanitized: any = {}; // Only include fields that are provided (partial updates) if (data['First Name'] !== undefined) sanitized['First Name'] = data['First Name'].trim(); if (data['Last Name'] !== undefined) sanitized['Last Name'] = data['Last Name'].trim(); if (data.Email !== undefined) sanitized['Email'] = data.Email.trim().toLowerCase(); if (data.Phone !== undefined) sanitized.Phone = data.Phone ? data.Phone.trim() : null; if (data.Nationality !== undefined) sanitized.Nationality = data.Nationality ? data.Nationality.trim() : null; if (data.Address !== undefined) sanitized.Address = data.Address ? data.Address.trim() : null; if (data['Date of Birth'] !== undefined) sanitized['Date of Birth'] = data['Date of Birth']; if (data['Member Since'] !== undefined) sanitized['Member Since'] = data['Member Since']; if (data['Membership Date Paid'] !== undefined) sanitized['Membership Date Paid'] = data['Membership Date Paid']; if (data['Payment Due Date'] !== undefined) sanitized['Payment Due Date'] = data['Payment Due Date']; // Boolean fields if (data['Current Year Dues Paid'] !== undefined) { sanitized['Current Year Dues Paid'] = Boolean(data['Current Year Dues Paid']); } // Enum fields if (data['Membership Status'] !== undefined) { sanitized['Membership Status'] = data['Membership Status']; } return sanitized; } function isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } function formatPhoneNumber(phone: string): string { if (!phone) return ''; const cleaned = phone.replace(/\D/g, ''); if (cleaned.length === 10) { return `(${cleaned.substring(0, 3)}) ${cleaned.substring(3, 6)}-${cleaned.substring(6)}`; } else if (cleaned.length === 11 && cleaned.startsWith('1')) { return `+1 (${cleaned.substring(1, 4)}) ${cleaned.substring(4, 7)}-${cleaned.substring(7)}`; } return phone; }