diff --git a/ENVIRONMENT_VARIABLES.md b/ENVIRONMENT_VARIABLES.md new file mode 100644 index 0000000..7a2d7ba --- /dev/null +++ b/ENVIRONMENT_VARIABLES.md @@ -0,0 +1,102 @@ +# Environment Variables Configuration + +## NocoDB Configuration (Required) + +To fix API key issues and improve container deployment, set these environment variables in your Docker container: + +### Required Variables + +```bash +# NocoDB Database Connection +NUXT_NOCODB_URL=https://database.monacousa.org +NUXT_NOCODB_TOKEN=your_actual_nocodb_api_token_here +NUXT_NOCODB_BASE_ID=your_nocodb_base_id_here +``` + +### Alternative Variable Names (also supported) + +```bash +# Alternative formats that also work +NOCODB_URL=https://database.monacousa.org +NOCODB_TOKEN=your_actual_nocodb_api_token_here +NOCODB_API_TOKEN=your_actual_nocodb_api_token_here +NOCODB_BASE_ID=your_nocodb_base_id_here +``` + +## How to Set in Docker + +### Option 1: Docker Compose (Recommended) + +Add to your `docker-compose.yml`: + +```yaml +services: + monacousa-portal: + image: your-image + environment: + - NUXT_NOCODB_URL=https://database.monacousa.org + - NUXT_NOCODB_TOKEN=your_actual_nocodb_api_token_here + - NUXT_NOCODB_BASE_ID=your_nocodb_base_id_here + # ... rest of your config +``` + +### Option 2: Docker Run Command + +```bash +docker run -d \ + -e NUXT_NOCODB_URL=https://database.monacousa.org \ + -e NUXT_NOCODB_TOKEN=your_actual_nocodb_api_token_here \ + -e NUXT_NOCODB_BASE_ID=your_nocodb_base_id_here \ + your-image +``` + +### Option 3: Environment File + +Create `.env` file: +```bash +NUXT_NOCODB_URL=https://database.monacousa.org +NUXT_NOCODB_TOKEN=your_actual_nocodb_api_token_here +NUXT_NOCODB_BASE_ID=your_nocodb_base_id_here +``` + +Then use: +```bash +docker run --env-file .env your-image +``` + +## Priority Order + +The system will check configuration in this order: + +1. **Environment Variables** (highest priority) +2. Admin Panel Configuration (fallback) +3. Runtime Config (last resort) + +## Benefits + +✅ **Container-Friendly**: No need to configure through web UI +✅ **Secure**: API tokens stored as environment variables +✅ **Reliable**: No Unicode/formatting issues +✅ **Version Control**: Can be managed in deployment configs +✅ **Scalable**: Same config across multiple containers + +## Getting Your Values + +### NocoDB API Token +1. Go to your NocoDB instance +2. Click your profile → API Tokens +3. Create new token or copy existing one +4. Use the raw token without any formatting + +### NocoDB Base ID +1. In NocoDB, go to your base +2. Check the URL: `https://your-nocodb.com/dashboard/#/nc/base/BASE_ID_HERE` +3. Copy the BASE_ID part + +## Testing Configuration + +After setting environment variables, check the logs: +- ✅ `[nocodb] ✅ Using environment variables - URL: https://database.monacousa.org` +- ✅ `[nocodb] ✅ Configuration validated successfully` + +If you see fallback messages, the environment variables aren't being read correctly. diff --git a/components/AdminConfigurationDialog.vue b/components/AdminConfigurationDialog.vue index 61f5020..f85f57e 100644 --- a/components/AdminConfigurationDialog.vue +++ b/components/AdminConfigurationDialog.vue @@ -57,39 +57,75 @@ - +
+ + +
- +
+ + +
- +
+ + +
@@ -97,42 +133,78 @@ - +
+ + +
Configure the table ID for the Members functionality
- +
+ + +
Configure the table ID for the Events functionality
- +
+ + +
Configure the table ID for the Event RSVPs functionality
@@ -186,32 +258,56 @@ - +
+ + +
- +
+ + +
@@ -249,17 +345,29 @@ - +
+ + +
@@ -267,29 +375,53 @@ - +
+ + +
- +
+ + +
@@ -335,32 +467,56 @@ - +
+ + +
- +
+ + +
@@ -377,55 +533,103 @@ - +
+ + +
- +
+ + +
- +
+ + +
- +
+ + +
@@ -592,6 +796,32 @@ const emailTestStatus = ref<{ success: boolean; message: string } | null>(null); // Test email address const testEmailAddress = ref(''); +// Editing state for fields (to prevent autofill interference) +const editingFields = ref({ + nocodbUrl: false, + nocodbApiKey: false, + nocodbBaseId: false, + membersTableId: false, + eventsTableId: false, + rsvpsTableId: false, + recaptchaSiteKey: false, + recaptchaSecretKey: false, + membershipFee: false, + iban: false, + accountHolder: false, + smtpHost: false, + smtpPort: false, + smtpUsername: false, + smtpPassword: false, + smtpFromAddress: false, + smtpFromName: false +}); + +// Toggle edit mode for a field +const toggleEdit = (fieldName: keyof typeof editingFields.value) => { + editingFields.value[fieldName] = !editingFields.value[fieldName]; +}; + // Form data const nocodbForm = ref({ url: 'https://database.monacousa.org', diff --git a/server/api/admin/fix-event-attendee-counts.post.ts b/server/api/admin/fix-event-attendee-counts.post.ts new file mode 100644 index 0000000..09a5f9c --- /dev/null +++ b/server/api/admin/fix-event-attendee-counts.post.ts @@ -0,0 +1,96 @@ +// server/api/admin/fix-event-attendee-counts.post.ts +import { createSessionManager } from '~/server/utils/session'; +import { createNocoDBEventsClient } from '~/server/utils/nocodb-events'; + +export default defineEventHandler(async (event) => { + try { + console.log('[admin/fix-event-attendee-counts] Starting attendee count fix...'); + + // Verify admin session + const sessionManager = createSessionManager(); + const cookieHeader = getHeader(event, 'cookie'); + const session = sessionManager.getSession(cookieHeader); + + if (!session?.user || session.user.tier !== 'admin') { + throw createError({ + statusCode: 403, + statusMessage: 'Admin access required' + }); + } + + console.log('[admin/fix-event-attendee-counts] Authorized admin user:', session.user.email); + + const eventsClient = createNocoDBEventsClient(); + + // Get all events + const eventsResponse = await eventsClient.findAll({ limit: 1000 }); + const events = eventsResponse.list || []; + + console.log('[admin/fix-event-attendee-counts] Found', events.length, 'events to process'); + + const results = []; + + // Process each event + for (const eventObj of events) { + try { + const eventId = eventObj.event_id || eventObj.id || (eventObj as any).Id; + const eventTitle = eventObj.title || 'Unknown Event'; + + console.log('[admin/fix-event-attendee-counts] Processing event:', eventTitle, 'ID:', eventId); + + // Force update the attendee count + const newCount = await eventsClient.forceUpdateEventAttendeeCount(eventId.toString()); + + results.push({ + event_id: eventId, + title: eventTitle, + old_count: eventObj.current_attendees || 0, + new_count: newCount, + status: 'success' + }); + + console.log('[admin/fix-event-attendee-counts] ✅ Fixed event:', eventTitle, 'Count:', newCount); + + } catch (eventError: any) { + console.error('[admin/fix-event-attendee-counts] ❌ Error processing event:', eventObj.title, eventError); + + results.push({ + event_id: eventObj.event_id || eventObj.id, + title: eventObj.title || 'Unknown Event', + old_count: eventObj.current_attendees || 0, + new_count: 0, + status: 'error', + error: eventError.message + }); + } + } + + const successCount = results.filter(r => r.status === 'success').length; + const errorCount = results.filter(r => r.status === 'error').length; + + console.log('[admin/fix-event-attendee-counts] ✅ Fix completed. Success:', successCount, 'Errors:', errorCount); + + return { + success: true, + message: `Fixed attendee counts for ${successCount} events (${errorCount} errors)`, + data: { + total_events: events.length, + success_count: successCount, + error_count: errorCount, + results: results + } + }; + + } catch (error: any) { + console.error('[admin/fix-event-attendee-counts] ❌ Error:', error); + + if (error.statusCode) { + throw error; + } + + throw createError({ + statusCode: 500, + statusMessage: 'Failed to fix event attendee counts' + }); + } +}); diff --git a/server/api/events/[id]/rsvp.post.ts b/server/api/events/[id]/rsvp.post.ts index 503a402..69453e0 100644 --- a/server/api/events/[id]/rsvp.post.ts +++ b/server/api/events/[id]/rsvp.post.ts @@ -135,12 +135,12 @@ export default defineEventHandler(async (event) => { const newRSVP = await eventsClient.createRSVP(rsvpData); - // Update event attendee count + // Update event attendee count using the new force update method try { - await updateEventAttendeeCount(eventId); - console.log('[RSVP] ✅ Updated event attendee count for event:', eventId); + const newAttendeeCount = await eventsClient.forceUpdateEventAttendeeCount(eventId); + console.log('[RSVP] ✅ Force updated event attendee count for event:', eventId, 'to:', newAttendeeCount); } catch (countError) { - console.log('[RSVP] ⚠️ Failed to update attendee count:', countError); + console.log('[RSVP] ⚠️ Failed to force update attendee count:', countError); // Don't fail the RSVP creation if count update fails } diff --git a/server/utils/nocodb-events.ts b/server/utils/nocodb-events.ts index f5a6b56..8b09311 100644 --- a/server/utils/nocodb-events.ts +++ b/server/utils/nocodb-events.ts @@ -180,31 +180,9 @@ export const normalizeEventFieldsFromNocoDB = (data: any): Event => { * Following the same pattern as the working members client */ export function createNocoDBEventsClient() { - // Validate API token before using it (from incoming version) + // Use the centralized configuration from nocodb.ts which now prioritizes environment variables const config = getNocoDbConfiguration(); - const token = config.token; - - if (token) { - const cleanToken = token.trim(); - - // Check for non-ASCII characters that would cause ByteString errors - if (!/^[\x00-\xFF]*$/.test(cleanToken)) { - console.error('[nocodb-events] ❌ CRITICAL ERROR: API token contains invalid Unicode characters!'); - throw createError({ - statusCode: 500, - statusMessage: 'Events system: NocoDB API token contains invalid characters. Please reconfigure the database connection.' - }); - } - - // Additional validation for common token issues - if (cleanToken.includes('•') || cleanToken.includes('…') || cleanToken.includes('"') || cleanToken.includes('"')) { - console.error('[nocodb-events] ❌ CRITICAL ERROR: API token contains formatting characters!'); - throw createError({ - statusCode: 500, - statusMessage: 'Events system: NocoDB API token contains formatting characters. Please reconfigure with the raw token from NocoDB.' - }); - } - } + console.log('[nocodb-events] ✅ Using NocoDB configuration (prioritizes environment variables)'); const eventsClient = { /** @@ -737,6 +715,29 @@ export function createNocoDBEventsClient() { console.error('[nocodb-events] ❌ Error calculating attendee count for event:', eventId, error); return 0; // Return 0 if calculation fails } + }, + + /** + * Force update attendee count for an event and save to database + */ + async forceUpdateEventAttendeeCount(eventId: string): Promise { + console.log('[nocodb-events] Force updating attendee count for event:', eventId); + + try { + // Calculate the current attendee count + const newCount = await this.calculateEventAttendeeCount(eventId); + + // Update the event's current_attendees field directly + await this.update(eventId, { + current_attendees: newCount.toString() + }); + + console.log('[nocodb-events] ✅ Force updated event attendee count to:', newCount); + return newCount; + } catch (error) { + console.error('[nocodb-events] ❌ Error force updating attendee count:', error); + return 0; + } } }; diff --git a/server/utils/nocodb.ts b/server/utils/nocodb.ts index 3fa38e9..2114cad 100644 --- a/server/utils/nocodb.ts +++ b/server/utils/nocodb.ts @@ -212,17 +212,29 @@ export const setGlobalNocoDBConfig = (config: any) => { export const getNocoDbConfiguration = () => { let configToUse: any = null; - // Try to use the global configuration first - if (globalNocoDBConfig) { - console.log('[nocodb] Using global configuration - URL:', globalNocoDBConfig.url); + // 1. PRIORITY: Check for environment variables first (container-friendly) + const envConfig = { + url: process.env.NUXT_NOCODB_URL || process.env.NOCODB_URL, + token: process.env.NUXT_NOCODB_TOKEN || process.env.NOCODB_TOKEN || process.env.NOCODB_API_TOKEN, + baseId: process.env.NUXT_NOCODB_BASE_ID || process.env.NOCODB_BASE_ID + }; + + if (envConfig.url && envConfig.token) { + console.log('[nocodb] ✅ Using environment variables - URL:', envConfig.url); + configToUse = envConfig; + } + // 2. FALLBACK: Try to use the global admin panel configuration + else if (globalNocoDBConfig) { + console.log('[nocodb] Using admin panel configuration - URL:', globalNocoDBConfig.url); configToUse = { url: globalNocoDBConfig.url, token: globalNocoDBConfig.token, baseId: globalNocoDBConfig.baseId }; - } else { - // Fallback to runtime config - console.log('[nocodb] Global config not available, using runtime config'); + } + // 3. LAST RESORT: Runtime config + else { + console.log('[nocodb] ⚠️ Using fallback runtime config'); const config = useRuntimeConfig().nocodb; configToUse = { ...config, @@ -231,33 +243,42 @@ export const getNocoDbConfiguration = () => { console.log('[nocodb] Fallback configuration URL:', configToUse.url); } - // Validate API token before using it - if (configToUse.token) { - const token = configToUse.token.trim(); - - // Check for non-ASCII characters that would cause ByteString errors - if (!/^[\x00-\xFF]*$/.test(token)) { - console.error('[nocodb] ❌ CRITICAL ERROR: API token contains invalid Unicode characters!'); - console.error('[nocodb] This will cause ByteString conversion errors in HTTP headers.'); - console.error('[nocodb] Please update the API token in the admin configuration.'); - throw createError({ - statusCode: 500, - statusMessage: 'NocoDB API token contains invalid characters. Please reconfigure the database connection in the admin panel with a valid API token.' - }); - } - - // Additional validation for common token issues - if (token.includes('•') || token.includes('…') || token.includes('"') || token.includes('"')) { - console.error('[nocodb] ❌ CRITICAL ERROR: API token contains formatting characters!'); - console.error('[nocodb] Found characters like bullets (•), quotes, etc. that break HTTP headers.'); - console.error('[nocodb] Please copy the raw API token from NocoDB without any formatting.'); - throw createError({ - statusCode: 500, - statusMessage: 'NocoDB API token contains formatting characters (bullets, quotes, etc.). Please reconfigure with the raw token from NocoDB.' - }); - } + // Validate configuration completeness + if (!configToUse.url || !configToUse.token) { + console.error('[nocodb] ❌ CRITICAL ERROR: Incomplete NocoDB configuration!'); + console.error('[nocodb] URL:', configToUse.url ? 'SET' : 'MISSING'); + console.error('[nocodb] Token:', configToUse.token ? 'SET' : 'MISSING'); + console.error('[nocodb] Set environment variables: NUXT_NOCODB_URL, NUXT_NOCODB_TOKEN'); + throw createError({ + statusCode: 500, + statusMessage: 'NocoDB configuration incomplete. Set NUXT_NOCODB_URL and NUXT_NOCODB_TOKEN environment variables.' + }); } + // Validate API token before using it + const token = configToUse.token.trim(); + + // Check for non-ASCII characters that would cause ByteString errors + if (!/^[\x00-\xFF]*$/.test(token)) { + console.error('[nocodb] ❌ CRITICAL ERROR: API token contains invalid Unicode characters!'); + console.error('[nocodb] This will cause ByteString conversion errors in HTTP headers.'); + throw createError({ + statusCode: 500, + statusMessage: 'NocoDB API token contains invalid characters. Please set a valid NUXT_NOCODB_TOKEN environment variable.' + }); + } + + // Additional validation for common token issues + if (token.includes('•') || token.includes('…') || token.includes('"') || token.includes('"')) { + console.error('[nocodb] ❌ CRITICAL ERROR: API token contains formatting characters!'); + console.error('[nocodb] Found characters like bullets (•), quotes, etc. that break HTTP headers.'); + throw createError({ + statusCode: 500, + statusMessage: 'NocoDB API token contains formatting characters. Please set a clean NUXT_NOCODB_TOKEN environment variable.' + }); + } + + console.log('[nocodb] ✅ Configuration validated successfully'); return configToUse; };