2025-08-07 23:44:28 +02:00
|
|
|
import { updateMember, getMemberById, handleNocoDbError } from '~/server/utils/nocodb';
|
2025-08-07 19:20:29 +02:00
|
|
|
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
|
2025-08-07 23:05:46 +02:00
|
|
|
await updateMember(id, memberData);
|
2025-08-07 19:20:29 +02:00
|
|
|
|
2025-08-07 23:05:46 +02:00
|
|
|
console.log('[api/members/[id].put] ✅ Member updated successfully, fetching complete record:', id);
|
2025-08-07 19:20:29 +02:00
|
|
|
|
2025-08-07 23:05:46 +02:00
|
|
|
// Fetch the complete updated member record to ensure we have all fields
|
|
|
|
|
const completeMember = await getMemberById(id);
|
|
|
|
|
console.log('[api/members/[id].put] Complete member fetched with fields:', Object.keys(completeMember));
|
|
|
|
|
|
|
|
|
|
// Return processed member with computed fields
|
2025-08-07 19:20:29 +02:00
|
|
|
const processedMember = {
|
2025-08-07 23:05:46 +02:00
|
|
|
...completeMember,
|
|
|
|
|
FullName: `${completeMember.first_name || ''} ${completeMember.last_name || ''}`.trim(),
|
|
|
|
|
FormattedPhone: formatPhoneNumber(completeMember.phone)
|
2025-08-07 19:20:29 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-07 23:05:46 +02:00
|
|
|
console.log('[api/members/[id].put] Processed member FullName:', processedMember.FullName);
|
|
|
|
|
console.log('[api/members/[id].put] Processed member nationality:', processedMember.nationality);
|
|
|
|
|
|
2025-08-07 19:20:29 +02:00
|
|
|
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)
|
2025-08-07 21:50:02 +02:00
|
|
|
if (data.first_name !== undefined) {
|
|
|
|
|
if (!data.first_name || typeof data.first_name !== 'string' || data.first_name.trim().length < 2) {
|
2025-08-07 19:20:29 +02:00
|
|
|
errors.push('First Name must be at least 2 characters');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-07 21:50:02 +02:00
|
|
|
if (data.last_name !== undefined) {
|
|
|
|
|
if (!data.last_name || typeof data.last_name !== 'string' || data.last_name.trim().length < 2) {
|
2025-08-07 19:20:29 +02:00
|
|
|
errors.push('Last Name must be at least 2 characters');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-07 21:50:02 +02:00
|
|
|
if (data.email !== undefined) {
|
|
|
|
|
if (!data.email || typeof data.email !== 'string' || !isValidEmail(data.email)) {
|
2025-08-07 19:20:29 +02:00
|
|
|
errors.push('Valid email address is required');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Optional field validation
|
2025-08-07 21:50:02 +02:00
|
|
|
if (data.phone !== undefined && data.phone && typeof data.phone === 'string' && data.phone.trim()) {
|
2025-08-07 19:20:29 +02:00
|
|
|
const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/;
|
2025-08-07 21:50:02 +02:00
|
|
|
const cleanPhone = data.phone.replace(/\D/g, '');
|
2025-08-07 19:20:29 +02:00
|
|
|
if (!phoneRegex.test(cleanPhone)) {
|
|
|
|
|
errors.push('Phone number format is invalid');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-07 21:50:02 +02:00
|
|
|
if (data.membership_status !== undefined && !['Active', 'Inactive', 'Pending', 'Expired'].includes(data.membership_status)) {
|
2025-08-07 19:20:29 +02:00
|
|
|
errors.push('Invalid membership status');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sanitizeMemberUpdateData(data: any): Partial<Member> {
|
|
|
|
|
const sanitized: any = {};
|
|
|
|
|
|
|
|
|
|
// Only include fields that are provided (partial updates)
|
2025-08-07 21:50:02 +02:00
|
|
|
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;
|
2025-08-07 19:20:29 +02:00
|
|
|
|
|
|
|
|
// Boolean fields
|
2025-08-07 21:50:02 +02:00
|
|
|
if (data.current_year_dues_paid !== undefined) {
|
|
|
|
|
sanitized.current_year_dues_paid = Boolean(data.current_year_dues_paid) ? 'true' : 'false';
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enum fields
|
2025-08-07 21:50:02 +02:00
|
|
|
if (data.membership_status !== undefined) {
|
|
|
|
|
sanitized.membership_status = data.membership_status;
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|