Add global branding and implement member ID system
All checks were successful
Build And Push Image / docker (push) Successful in 3m2s
All checks were successful
Build And Push Image / docker (push) Successful in 3m2s
- Add MonacoUSA logo component with global header placement - Implement member ID generation and migration system - Create profile page and improve dashboard navigation - Add member ID as payment reference in dues banner - Enable support contact functionality with pre-filled email
This commit is contained in:
117
server/api/admin/migrate-member-ids.post.ts
Normal file
117
server/api/admin/migrate-member-ids.post.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { createSessionManager } from '~/server/utils/session';
|
||||
import { findMembersWithoutMemberID, generateMemberID } from '~/server/utils/member-id';
|
||||
import { updateMember, handleNocoDbError } from '~/server/utils/nocodb';
|
||||
import type { Member } from '~/utils/types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[api/admin/migrate-member-ids.post] =========================');
|
||||
console.log('[api/admin/migrate-member-ids.post] POST /api/admin/migrate-member-ids - Migrate existing member IDs');
|
||||
console.log('[api/admin/migrate-member-ids.post] Request from:', getClientIP(event));
|
||||
|
||||
try {
|
||||
// Validate session and require Admin 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 !== 'admin') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Administrator privileges required for member ID migration'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[api/admin/migrate-member-ids.post] Authorized admin user:', session.user.email);
|
||||
|
||||
// Find all members without member IDs
|
||||
console.log('[api/admin/migrate-member-ids.post] Finding members without member IDs...');
|
||||
const membersWithoutId = await findMembersWithoutMemberID();
|
||||
|
||||
if (membersWithoutId.length === 0) {
|
||||
console.log('[api/admin/migrate-member-ids.post] No members found without member IDs');
|
||||
return {
|
||||
success: true,
|
||||
message: 'All members already have member IDs assigned',
|
||||
migrated: 0,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[api/admin/migrate-member-ids.post] Found ${membersWithoutId.length} members without member IDs`);
|
||||
|
||||
const migrationResults = [];
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// Migrate each member
|
||||
for (let i = 0; i < membersWithoutId.length; i++) {
|
||||
const member = membersWithoutId[i];
|
||||
console.log(`[api/admin/migrate-member-ids.post] Migrating member ${i + 1}/${membersWithoutId.length}: ${member.first_name} ${member.last_name} (ID: ${member.Id})`);
|
||||
|
||||
try {
|
||||
// Generate unique member ID
|
||||
const memberID = await generateMemberID();
|
||||
console.log(`[api/admin/migrate-member-ids.post] Generated ID ${memberID} for member ${member.Id}`);
|
||||
|
||||
// Update member with new member ID
|
||||
await updateMember(member.Id, { member_id: memberID });
|
||||
|
||||
migrationResults.push({
|
||||
memberId: member.Id,
|
||||
memberName: `${member.first_name} ${member.last_name}`,
|
||||
generatedId: memberID,
|
||||
success: true
|
||||
});
|
||||
|
||||
successCount++;
|
||||
console.log(`[api/admin/migrate-member-ids.post] ✅ Successfully migrated member ${member.Id} with ID ${memberID}`);
|
||||
|
||||
// Add a small delay to avoid overwhelming the database
|
||||
if (i < membersWithoutId.length - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`[api/admin/migrate-member-ids.post] ❌ Failed to migrate member ${member.Id}:`, error);
|
||||
|
||||
migrationResults.push({
|
||||
memberId: member.Id,
|
||||
memberName: `${member.first_name} ${member.last_name}`,
|
||||
generatedId: null,
|
||||
success: false,
|
||||
error: error.message || 'Unknown error'
|
||||
});
|
||||
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[api/admin/migrate-member-ids.post] =========================');
|
||||
console.log('[api/admin/migrate-member-ids.post] Migration completed');
|
||||
console.log('[api/admin/migrate-member-ids.post] Total members processed:', membersWithoutId.length);
|
||||
console.log('[api/admin/migrate-member-ids.post] Successful migrations:', successCount);
|
||||
console.log('[api/admin/migrate-member-ids.post] Failed migrations:', errorCount);
|
||||
console.log('[api/admin/migrate-member-ids.post] =========================');
|
||||
|
||||
return {
|
||||
success: errorCount === 0,
|
||||
message: `Migration completed. ${successCount} members successfully migrated, ${errorCount} errors.`,
|
||||
migrated: successCount,
|
||||
errors: errorCount,
|
||||
total: membersWithoutId.length,
|
||||
results: migrationResults
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[api/admin/migrate-member-ids.post] ❌ Migration failed:', error);
|
||||
handleNocoDbError(error, 'migrateMemberIDs', 'Member ID Migration');
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createMember, handleNocoDbError } from '~/server/utils/nocodb';
|
||||
import { createSessionManager } from '~/server/utils/session';
|
||||
import { generateMemberID } from '~/server/utils/member-id';
|
||||
import type { Member, MembershipStatus } from '~/utils/types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -50,7 +51,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
// Sanitize and prepare data
|
||||
const memberData = sanitizeMemberData(normalizedBody);
|
||||
const memberData = await sanitizeMemberData(normalizedBody);
|
||||
console.log('[api/members.post] Sanitized data fields:', Object.keys(memberData));
|
||||
|
||||
// Create member in NocoDB
|
||||
@@ -109,9 +110,14 @@ function validateMemberData(data: any): string[] {
|
||||
return errors;
|
||||
}
|
||||
|
||||
function sanitizeMemberData(data: any): Partial<Member> {
|
||||
async function sanitizeMemberData(data: any): Promise<Partial<Member>> {
|
||||
const sanitized: any = {};
|
||||
|
||||
// Generate unique member ID
|
||||
console.log('[api/members.post] Generating member ID for new member...');
|
||||
sanitized.member_id = await generateMemberID();
|
||||
console.log('[api/members.post] Generated member ID:', sanitized.member_id);
|
||||
|
||||
// Required fields
|
||||
sanitized.first_name = data.first_name.trim();
|
||||
sanitized.last_name = data.last_name.trim();
|
||||
|
||||
123
server/utils/member-id.ts
Normal file
123
server/utils/member-id.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { getMembers, updateMember } from './nocodb';
|
||||
import type { Member } from '~/utils/types';
|
||||
|
||||
/**
|
||||
* Generates a unique member ID in the format MUSA-{unique 6-digit number}
|
||||
* Checks against existing member IDs to ensure uniqueness
|
||||
* @returns Promise<string> - The unique member ID
|
||||
*/
|
||||
export async function generateMemberID(): Promise<string> {
|
||||
console.log('[member-id] Generating new member ID...');
|
||||
|
||||
let memberID: string;
|
||||
let isUnique = false;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 100; // Prevent infinite loops
|
||||
|
||||
while (!isUnique && attempts < maxAttempts) {
|
||||
attempts++;
|
||||
|
||||
// Generate a 6-digit number (100000 to 999999)
|
||||
const uniqueNumber = Math.floor(Math.random() * 900000) + 100000;
|
||||
memberID = `MUSA-${uniqueNumber}`;
|
||||
|
||||
console.log(`[member-id] Attempt ${attempts}: Checking uniqueness of ${memberID}`);
|
||||
|
||||
// Check if ID already exists in database
|
||||
const existingMember = await checkMemberIDExists(memberID);
|
||||
isUnique = !existingMember;
|
||||
|
||||
if (!isUnique) {
|
||||
console.log(`[member-id] ID ${memberID} already exists, generating new one...`);
|
||||
}
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
console.error('[member-id] Failed to generate unique member ID after maximum attempts');
|
||||
throw new Error('Failed to generate unique member ID after maximum attempts');
|
||||
}
|
||||
|
||||
console.log(`[member-id] ✅ Generated unique member ID: ${memberID!} (attempts: ${attempts})`);
|
||||
return memberID!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a member ID already exists in the database
|
||||
* @param memberID - The member ID to check
|
||||
* @returns Promise<boolean> - True if the member ID exists, false otherwise
|
||||
*/
|
||||
export async function checkMemberIDExists(memberID: string): Promise<boolean> {
|
||||
try {
|
||||
console.log(`[member-id] Checking if member ID exists: ${memberID}`);
|
||||
|
||||
// Get all members and check for duplicate member_id
|
||||
const members = await getMembers();
|
||||
const memberList = Array.isArray(members) ? members : members?.list || [];
|
||||
|
||||
const existingMember = memberList.find((member: Member) => member.member_id === memberID);
|
||||
const exists = !!existingMember;
|
||||
|
||||
console.log(`[member-id] Member ID ${memberID} exists: ${exists}`);
|
||||
return exists;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[member-id] Error checking member ID existence:', error);
|
||||
// In case of error, assume it doesn't exist to allow generation to continue
|
||||
// The actual creation will fail if there's a real database issue
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all members without a member_id field
|
||||
* Used for migration purposes
|
||||
* @returns Promise<Member[]> - Array of members without member IDs
|
||||
*/
|
||||
export async function findMembersWithoutMemberID(): Promise<Member[]> {
|
||||
try {
|
||||
console.log('[member-id] Finding members without member IDs for migration...');
|
||||
|
||||
const members = await getMembers();
|
||||
const memberList = Array.isArray(members) ? members : members?.list || [];
|
||||
|
||||
const membersWithoutId = memberList.filter((member: Member) =>
|
||||
!member.member_id || member.member_id.trim() === ''
|
||||
);
|
||||
|
||||
console.log(`[member-id] Found ${membersWithoutId.length} members without member IDs`);
|
||||
return membersWithoutId;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[member-id] Error finding members without member IDs:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a member ID format
|
||||
* @param memberID - The member ID to validate
|
||||
* @returns boolean - True if valid format, false otherwise
|
||||
*/
|
||||
export function isValidMemberIDFormat(memberID: string): boolean {
|
||||
if (!memberID || typeof memberID !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check format: MUSA-{6 digits}
|
||||
const memberIDRegex = /^MUSA-\d{6}$/;
|
||||
return memberIDRegex.test(memberID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the numeric part from a member ID
|
||||
* @param memberID - The member ID (e.g., "MUSA-123456")
|
||||
* @returns number - The numeric part or null if invalid
|
||||
*/
|
||||
export function extractMemberIDNumber(memberID: string): number | null {
|
||||
if (!isValidMemberIDFormat(memberID)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numericPart = memberID.replace('MUSA-', '');
|
||||
return parseInt(numericPart, 10);
|
||||
}
|
||||
Reference in New Issue
Block a user