#### __1. Role-Based Security Architecture__
All checks were successful
Build And Push Image / docker (push) Successful in 2m58s
All checks were successful
Build And Push Image / docker (push) Successful in 2m58s
- Replaces group-based tiers with proper Keycloak realm roles - `monaco-user`, `monaco-board`, `monaco-admin` roles - Backward compatibility with existing group system #### __2. Advanced User Management__ - Comprehensive user profile synchronization - Membership data stored in Keycloak user attributes - Bidirectional sync between NocoDB and Keycloak #### __3. Session Security & Monitoring__ - Real-time session tracking and management - Administrative session control capabilities - Enhanced security analytics foundation #### __4. Email Workflow System__ - Multiple email types: DUES_REMINDER, MEMBERSHIP_RENEWAL, WELCOME, VERIFICATION - Customizable email parameters and lifespans - Advanced email template support #### __5. Seamless Migration Path__ - All existing functionality continues to work - New users automatically get realm roles - Gradual migration from groups to roles - Zero breaking changes ### 🔧 __What You Can Do Now__ #### __For New Users:__ - Public registrations automatically assign `monaco-user` role - Portal account creation syncs member data to Keycloak attributes - Enhanced email verification and welcome workflows #### __For Administrators:__ - Session management and monitoring capabilities - Advanced user profile management with member data sync - Comprehensive role assignment and management - Enhanced email communication workflows #### __For Developers:__ - Use `hasRole('monaco-admin')` for role-based checks - Access `getAllRoles()` for debugging and analytics - Enhanced `useAuth()` composable with backward compatibility - Comprehensive TypeScript support throughout ### 🛡️ __Security & Reliability__ - __Backward Compatibility__: Existing users continue to work seamlessly - __Enhanced Security__: Proper realm role-based authorization - __Error Handling__: Comprehensive error handling and fallbacks - __Type Safety__: Full TypeScript support throughout the system
This commit is contained in:
163
server/api/admin/cleanup-accounts.post.ts
Normal file
163
server/api/admin/cleanup-accounts.post.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { Member } from '~/utils/types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[api/admin/cleanup-accounts.post] =========================');
|
||||
console.log('[api/admin/cleanup-accounts.post] POST /api/admin/cleanup-accounts - Account cleanup for expired members');
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// Require admin privileges for account cleanup
|
||||
if (session.user.tier !== 'admin') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Admin privileges required'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[api/admin/cleanup-accounts.post] Authorized admin:', session.user.email);
|
||||
|
||||
// Get cleanup options from request body (optional)
|
||||
const body = await readBody(event).catch(() => ({}));
|
||||
const dryRun = body?.dryRun === true;
|
||||
const monthsOverdue = body?.monthsOverdue || 3;
|
||||
|
||||
console.log('[api/admin/cleanup-accounts.post] Cleanup options:', { dryRun, monthsOverdue });
|
||||
|
||||
// Calculate cutoff date (default: 3 months ago)
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setMonth(cutoffDate.getMonth() - monthsOverdue);
|
||||
|
||||
console.log('[api/admin/cleanup-accounts.post] Cutoff date:', cutoffDate.toISOString());
|
||||
|
||||
// Find members registered before cutoff date with unpaid dues
|
||||
const { getMembers } = await import('~/server/utils/nocodb');
|
||||
const membersResult = await getMembers();
|
||||
const allMembers = membersResult.list || [];
|
||||
|
||||
const expiredMembers = allMembers.filter((member: Member) => {
|
||||
// Must have a registration date
|
||||
if (!member.registration_date) return false;
|
||||
|
||||
// Must be registered before cutoff date
|
||||
const registrationDate = new Date(member.registration_date);
|
||||
if (registrationDate >= cutoffDate) return false;
|
||||
|
||||
// Must have unpaid dues
|
||||
if (member.current_year_dues_paid === 'true') return false;
|
||||
|
||||
// Must have a Keycloak ID (portal account)
|
||||
if (!member.keycloak_id) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log('[api/admin/cleanup-accounts.post] Found', expiredMembers.length, 'expired members for cleanup');
|
||||
|
||||
const deletedAccounts = [];
|
||||
const failedDeletions = [];
|
||||
|
||||
if (!dryRun && expiredMembers.length > 0) {
|
||||
const { createKeycloakAdminClient } = await import('~/server/utils/keycloak-admin');
|
||||
const { deleteMember } = await import('~/server/utils/nocodb');
|
||||
const keycloakAdmin = createKeycloakAdminClient();
|
||||
|
||||
for (const member of expiredMembers) {
|
||||
try {
|
||||
console.log('[api/admin/cleanup-accounts.post] Processing cleanup for:', member.email);
|
||||
|
||||
// Delete from Keycloak first
|
||||
if (member.keycloak_id) {
|
||||
try {
|
||||
await keycloakAdmin.deleteUser(member.keycloak_id);
|
||||
console.log('[api/admin/cleanup-accounts.post] Deleted Keycloak user:', member.keycloak_id);
|
||||
} catch (keycloakError: any) {
|
||||
console.warn('[api/admin/cleanup-accounts.post] Failed to delete Keycloak user:', keycloakError.message);
|
||||
// Continue with member deletion even if Keycloak deletion fails
|
||||
}
|
||||
}
|
||||
|
||||
// Delete member record
|
||||
await deleteMember(member.Id);
|
||||
console.log('[api/admin/cleanup-accounts.post] Deleted member record:', member.Id);
|
||||
|
||||
deletedAccounts.push({
|
||||
id: member.Id,
|
||||
email: member.email,
|
||||
name: `${member.first_name} ${member.last_name}`,
|
||||
registrationDate: member.registration_date,
|
||||
keycloakId: member.keycloak_id
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[api/admin/cleanup-accounts.post] Failed to delete account for', member.email, ':', error);
|
||||
failedDeletions.push({
|
||||
id: member.Id,
|
||||
email: member.email,
|
||||
name: `${member.first_name} ${member.last_name}`,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
dryRun,
|
||||
monthsOverdue,
|
||||
cutoffDate: cutoffDate.toISOString(),
|
||||
totalExpiredMembers: expiredMembers.length,
|
||||
deletedCount: deletedAccounts.length,
|
||||
failedCount: failedDeletions.length,
|
||||
message: dryRun
|
||||
? `Found ${expiredMembers.length} expired accounts that would be deleted (dry run)`
|
||||
: `Cleaned up ${deletedAccounts.length} expired accounts${failedDeletions.length > 0 ? ` (${failedDeletions.length} failed)` : ''}`,
|
||||
data: {
|
||||
expiredMembers: expiredMembers.map(m => ({
|
||||
id: m.Id,
|
||||
email: m.email,
|
||||
name: `${m.first_name} ${m.last_name}`,
|
||||
registrationDate: m.registration_date,
|
||||
daysSinceRegistration: Math.floor((Date.now() - new Date(m.registration_date || '').getTime()) / (1000 * 60 * 60 * 24)),
|
||||
hasKeycloakAccount: !!m.keycloak_id
|
||||
})),
|
||||
deleted: deletedAccounts,
|
||||
failed: failedDeletions
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[api/admin/cleanup-accounts.post] ✅ Account cleanup completed');
|
||||
console.log('[api/admin/cleanup-accounts.post] Summary:', {
|
||||
found: expiredMembers.length,
|
||||
deleted: deletedAccounts.length,
|
||||
failed: failedDeletions.length,
|
||||
dryRun
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[api/admin/cleanup-accounts.post] ❌ Account cleanup failed:', error);
|
||||
|
||||
// If it's already an HTTP error, re-throw it
|
||||
if (error.statusCode) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Otherwise, wrap it in a generic error
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || 'Account cleanup failed'
|
||||
});
|
||||
}
|
||||
});
|
||||
43
server/api/admin/recaptcha-config.get.ts
Normal file
43
server/api/admin/recaptcha-config.get.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[api/admin/recaptcha-config.get] =========================');
|
||||
console.log('[api/admin/recaptcha-config.get] GET /api/admin/recaptcha-config - Get reCAPTCHA configuration');
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
if (session.user.tier !== 'admin') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Admin privileges required'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[api/admin/recaptcha-config.get] Authorized admin:', session.user.email);
|
||||
|
||||
// Get reCAPTCHA configuration
|
||||
const { getRecaptchaConfig } = await import('~/server/utils/admin-config');
|
||||
const config = getRecaptchaConfig();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
siteKey: config.siteKey,
|
||||
secretKey: config.secretKey ? '••••••••••••••••' : ''
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[api/admin/recaptcha-config.get] ❌ Error getting reCAPTCHA config:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
64
server/api/admin/recaptcha-config.post.ts
Normal file
64
server/api/admin/recaptcha-config.post.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[api/admin/recaptcha-config.post] =========================');
|
||||
console.log('[api/admin/recaptcha-config.post] POST /api/admin/recaptcha-config - Save reCAPTCHA configuration');
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
if (session.user.tier !== 'admin') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Admin privileges required'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[api/admin/recaptcha-config.post] Authorized admin:', session.user.email);
|
||||
|
||||
// Get and validate request body
|
||||
const body = await readBody(event);
|
||||
console.log('[api/admin/recaptcha-config.post] Request body fields:', Object.keys(body));
|
||||
|
||||
// Validate required fields
|
||||
if (!body.siteKey || typeof body.siteKey !== 'string') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Site Key is required'
|
||||
});
|
||||
}
|
||||
|
||||
if (!body.secretKey || typeof body.secretKey !== 'string') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Secret Key is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Save reCAPTCHA configuration
|
||||
const { saveRecaptchaConfig } = await import('~/server/utils/admin-config');
|
||||
await saveRecaptchaConfig({
|
||||
siteKey: body.siteKey.trim(),
|
||||
secretKey: body.secretKey.trim()
|
||||
}, session.user.email);
|
||||
|
||||
console.log('[api/admin/recaptcha-config.post] ✅ reCAPTCHA configuration saved successfully');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'reCAPTCHA configuration saved successfully'
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[api/admin/recaptcha-config.post] ❌ Error saving reCAPTCHA config:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
40
server/api/admin/registration-config.get.ts
Normal file
40
server/api/admin/registration-config.get.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[api/admin/registration-config.get] =========================');
|
||||
console.log('[api/admin/registration-config.get] GET /api/admin/registration-config - Get registration configuration');
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
if (session.user.tier !== 'admin') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Admin privileges required'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[api/admin/registration-config.get] Authorized admin:', session.user.email);
|
||||
|
||||
// Get registration configuration
|
||||
const { getRegistrationConfig } = await import('~/server/utils/admin-config');
|
||||
const config = getRegistrationConfig();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: config
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[api/admin/registration-config.get] ❌ Error getting registration config:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
72
server/api/admin/registration-config.post.ts
Normal file
72
server/api/admin/registration-config.post.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[api/admin/registration-config.post] =========================');
|
||||
console.log('[api/admin/registration-config.post] POST /api/admin/registration-config - Save registration configuration');
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
if (session.user.tier !== 'admin') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Admin privileges required'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[api/admin/registration-config.post] Authorized admin:', session.user.email);
|
||||
|
||||
// Get and validate request body
|
||||
const body = await readBody(event);
|
||||
console.log('[api/admin/registration-config.post] Request body fields:', Object.keys(body));
|
||||
|
||||
// Validate required fields
|
||||
if (!body.membershipFee || typeof body.membershipFee !== 'number' || body.membershipFee <= 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Valid membership fee is required'
|
||||
});
|
||||
}
|
||||
|
||||
if (!body.iban || typeof body.iban !== 'string') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'IBAN is required'
|
||||
});
|
||||
}
|
||||
|
||||
if (!body.accountHolder || typeof body.accountHolder !== 'string') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Account holder name is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Save registration configuration
|
||||
const { saveRegistrationConfig } = await import('~/server/utils/admin-config');
|
||||
await saveRegistrationConfig({
|
||||
membershipFee: body.membershipFee,
|
||||
iban: body.iban.trim(),
|
||||
accountHolder: body.accountHolder.trim()
|
||||
}, session.user.email);
|
||||
|
||||
console.log('[api/admin/registration-config.post] ✅ Registration configuration saved successfully');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Registration configuration saved successfully'
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[api/admin/registration-config.post] ❌ Error saving registration config:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user