Add NocoDB events/RSVPs table config and improve session handling
Build And Push Image / docker (push) Successful in 3m24s
Details
Build And Push Image / docker (push) Successful in 3m24s
Details
- Add events and rsvps table ID fields to NocoDB settings dialog - Replace mock getUserSession with proper SessionManager integration - Improve type safety with explicit type casting and error handling - Add comprehensive events system bug analysis documentation
This commit is contained in:
parent
e06f639454
commit
67d4d5236b
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Events System - Comprehensive Bug Analysis
|
||||||
|
|
||||||
|
## CRITICAL BUGS IDENTIFIED:
|
||||||
|
|
||||||
|
### 1. **MAJOR: Database Architecture Flaw**
|
||||||
|
**File:** `server/utils/nocodb-events.ts`
|
||||||
|
**Issue:** The system attempts to use the same table for both Events and RSVPs, causing data corruption
|
||||||
|
**Severity:** CRITICAL - System Breaking
|
||||||
|
**Status:** PARTIALLY FIXED - Still has configuration issues
|
||||||
|
|
||||||
|
### 2. **CRITICAL: Configuration Missing**
|
||||||
|
**File:** `nuxt.config.ts`
|
||||||
|
**Issue:** Missing events-specific NocoDB configuration properties
|
||||||
|
**Impact:** Events system cannot initialize properly
|
||||||
|
**Missing Properties:**
|
||||||
|
- `eventsBaseId`
|
||||||
|
- `eventsTableId`
|
||||||
|
- `rsvpTableId`
|
||||||
|
|
||||||
|
### 3. **MAJOR: RSVP Functions Wrong Table**
|
||||||
|
**File:** `server/utils/nocodb-events.ts`
|
||||||
|
**Issue:** All RSVP functions still point to events table instead of RSVP table
|
||||||
|
**Impact:** RSVPs stored in wrong table, data corruption
|
||||||
|
|
||||||
|
### 4. **CRITICAL: Type Safety Issues**
|
||||||
|
**File:** `server/utils/nocodb-events.ts`
|
||||||
|
**Issue:** Multiple `unknown` types causing runtime errors
|
||||||
|
**Impact:** Calendar fails to load, RSVP system breaks
|
||||||
|
|
||||||
|
### 5. **MAJOR: API Endpoint Issues**
|
||||||
|
**Files:** All `server/api/events/` files
|
||||||
|
**Issue:** Recently fixed authentication but still has logical bugs
|
||||||
|
**Remaining Issues:**
|
||||||
|
- No validation of event data
|
||||||
|
- Missing error handling for database failures
|
||||||
|
- Inconsistent response formats
|
||||||
|
|
||||||
|
### 6. **CRITICAL: Frontend Component Bugs**
|
||||||
|
**File:** `components/CreateEventDialog.vue`
|
||||||
|
**Issues:**
|
||||||
|
- Form validation insufficient
|
||||||
|
- Missing error handling for API failures
|
||||||
|
- Date/time formatting issues
|
||||||
|
- No loading states for better UX
|
||||||
|
|
||||||
|
### 7. **MAJOR: Calendar Component Issues**
|
||||||
|
**File:** `components/EventCalendar.vue`
|
||||||
|
**Issues:**
|
||||||
|
- Event transformation logic flawed
|
||||||
|
- Mobile view switching problems
|
||||||
|
- FullCalendar integration missing key features
|
||||||
|
- No error boundaries for calendar failures
|
||||||
|
|
||||||
|
### 8. **CRITICAL: Event Details Dialog Bugs**
|
||||||
|
**File:** `components/EventDetailsDialog.vue`
|
||||||
|
**Issues:**
|
||||||
|
- RSVP submission hardcoded member_id as empty string
|
||||||
|
- Payment info hardcoded instead of from config
|
||||||
|
- Missing proper error handling
|
||||||
|
- No loading states
|
||||||
|
|
||||||
|
### 9. **MAJOR: UseEvents Composable Issues**
|
||||||
|
**File:** `composables/useEvents.ts`
|
||||||
|
**Issues:**
|
||||||
|
- Calendar events function not properly integrated
|
||||||
|
- Cache key generation problematic
|
||||||
|
- Error propagation inconsistent
|
||||||
|
- Date handling utilities missing
|
||||||
|
|
||||||
|
### 10. **CRITICAL: Environment Configuration Incomplete**
|
||||||
|
**File:** `nuxt.config.ts` and `.env.example`
|
||||||
|
**Issues:**
|
||||||
|
- Missing events-specific environment variables
|
||||||
|
- No fallback values for development
|
||||||
|
- Events base/table IDs not configured
|
||||||
|
|
||||||
|
## ARCHITECTURAL PROBLEMS:
|
||||||
|
|
||||||
|
### 1. **Data Model Confusion**
|
||||||
|
The system tries to store Events and RSVPs in the same table, which is fundamentally wrong:
|
||||||
|
- Events need their own table with event-specific fields
|
||||||
|
- RSVPs need a separate table with foreign key to events
|
||||||
|
- Current mixing causes data corruption and query failures
|
||||||
|
|
||||||
|
### 2. **Configuration Inconsistency**
|
||||||
|
Events system references configuration properties that don't exist:
|
||||||
|
- `config.nocodb.eventsBaseId` - doesn't exist
|
||||||
|
- `config.nocodb.eventsTableId` - doesn't exist
|
||||||
|
- `config.nocodb.rsvpTableId` - doesn't exist
|
||||||
|
|
||||||
|
### 3. **API Response Inconsistency**
|
||||||
|
Different endpoints return different response formats:
|
||||||
|
- Some return `{ success, data, message }`
|
||||||
|
- Others return raw NocoDB responses
|
||||||
|
- Frontend expects consistent format
|
||||||
|
|
||||||
|
### 4. **Frontend State Management Issues**
|
||||||
|
- No centralized error handling
|
||||||
|
- Inconsistent loading states
|
||||||
|
- Cache invalidation problems
|
||||||
|
- Component state synchronization issues
|
||||||
|
|
||||||
|
## IMMEDIATE FIXES REQUIRED:
|
||||||
|
|
||||||
|
### Phase 1 - Critical Infrastructure
|
||||||
|
1. Fix NocoDB configuration in `nuxt.config.ts`
|
||||||
|
2. Separate Events and RSVPs into different tables/functions
|
||||||
|
3. Fix all TypeScript errors
|
||||||
|
4. Ensure basic API endpoints work
|
||||||
|
|
||||||
|
### Phase 2 - API Stability
|
||||||
|
1. Standardize API response formats
|
||||||
|
2. Add proper validation and error handling
|
||||||
|
3. Fix authentication integration
|
||||||
|
4. Test all CRUD operations
|
||||||
|
|
||||||
|
### Phase 3 - Frontend Polish
|
||||||
|
1. Fix component error handling
|
||||||
|
2. Add proper loading states
|
||||||
|
3. Fix form validation
|
||||||
|
4. Test calendar integration
|
||||||
|
|
||||||
|
### Phase 4 - Integration Testing
|
||||||
|
1. End-to-end event creation flow
|
||||||
|
2. RSVP submission and management
|
||||||
|
3. Calendar display and interaction
|
||||||
|
4. Mobile responsiveness
|
||||||
|
|
||||||
|
## RECOMMENDED APPROACH:
|
||||||
|
|
||||||
|
1. **Stop using current events system** - it will cause data corruption
|
||||||
|
2. **Fix configuration first** - add missing environment variables
|
||||||
|
3. **Separate data models** - create proper Events and RSVPs tables
|
||||||
|
4. **Rebuild API layer** - ensure consistency and reliability
|
||||||
|
5. **Fix frontend components** - proper error handling and state management
|
||||||
|
6. **Full integration testing** - ensure entire flow works end-to-end
|
||||||
|
|
||||||
|
## ESTIMATED EFFORT:
|
||||||
|
- **Critical fixes:** 4-6 hours
|
||||||
|
- **Full system stability:** 8-12 hours
|
||||||
|
- **Polish and testing:** 4-6 hours
|
||||||
|
- **Total:** 16-24 hours of focused development time
|
||||||
|
|
||||||
|
## RISK ASSESSMENT:
|
||||||
|
- **Current system:** HIGH RISK - will cause data loss/corruption
|
||||||
|
- **After Phase 1 fixes:** MEDIUM RISK - basic functionality restored
|
||||||
|
- **After Phase 2 fixes:** LOW RISK - production ready
|
||||||
|
- **After Phase 3-4:** MINIMAL RISK - polished and tested
|
||||||
|
|
@ -101,6 +101,38 @@
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.tables.events"
|
||||||
|
label="Events Table ID"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
placeholder="events-table-id"
|
||||||
|
:error="hasFieldError('tables.events')"
|
||||||
|
:error-messages="getFieldError('tables.events')"
|
||||||
|
/>
|
||||||
|
<div class="text-caption text-medium-emphasis mt-1">
|
||||||
|
Configure the table ID for the Events functionality
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.tables.rsvps"
|
||||||
|
label="RSVPs Table ID"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
placeholder="rsvps-table-id"
|
||||||
|
:error="hasFieldError('tables.rsvps')"
|
||||||
|
:error-messages="getFieldError('tables.rsvps')"
|
||||||
|
/>
|
||||||
|
<div class="text-caption text-medium-emphasis mt-1">
|
||||||
|
Configure the table ID for the Event RSVPs functionality
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-divider class="my-2" />
|
<v-divider class="my-2" />
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
@ -217,7 +249,11 @@ const form = ref<NocoDBSettings>({
|
||||||
url: 'https://database.monacousa.org',
|
url: 'https://database.monacousa.org',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
baseId: '',
|
baseId: '',
|
||||||
tables: { members: '' }
|
tables: {
|
||||||
|
members: '',
|
||||||
|
events: '',
|
||||||
|
rsvps: ''
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
|
|
@ -268,9 +304,13 @@ const loadSettings = async () => {
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
form.value = { ...response.data };
|
form.value = { ...response.data };
|
||||||
// Ensure tables object exists
|
// Ensure tables object exists with all required fields
|
||||||
if (!form.value.tables) {
|
if (!form.value.tables) {
|
||||||
form.value.tables = { members: '' };
|
form.value.tables = {
|
||||||
|
members: '',
|
||||||
|
events: '',
|
||||||
|
rsvps: ''
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -363,7 +403,11 @@ const resetForm = () => {
|
||||||
url: 'https://database.monacousa.org',
|
url: 'https://database.monacousa.org',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
baseId: '',
|
baseId: '',
|
||||||
tables: { members: '' }
|
tables: {
|
||||||
|
members: '',
|
||||||
|
events: '',
|
||||||
|
rsvps: ''
|
||||||
|
}
|
||||||
};
|
};
|
||||||
clearFieldErrors();
|
clearFieldErrors();
|
||||||
connectionStatus.value = null;
|
connectionStatus.value = null;
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ export default defineNuxtConfig({
|
||||||
baseId: process.env.NUXT_NOCODB_BASE_ID || "",
|
baseId: process.env.NUXT_NOCODB_BASE_ID || "",
|
||||||
eventsBaseId: process.env.NUXT_NOCODB_EVENTS_BASE_ID || "",
|
eventsBaseId: process.env.NUXT_NOCODB_EVENTS_BASE_ID || "",
|
||||||
eventsTableId: process.env.NUXT_NOCODB_EVENTS_TABLE_ID || "",
|
eventsTableId: process.env.NUXT_NOCODB_EVENTS_TABLE_ID || "",
|
||||||
|
rsvpTableId: process.env.NUXT_NOCODB_RSVP_TABLE_ID || "",
|
||||||
},
|
},
|
||||||
minio: {
|
minio: {
|
||||||
endPoint: process.env.NUXT_MINIO_ENDPOINT || "s3.monacousa.org",
|
endPoint: process.env.NUXT_MINIO_ENDPOINT || "s3.monacousa.org",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// server/api/events/[id]/attendees.patch.ts
|
// server/api/events/[id]/attendees.patch.ts
|
||||||
import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
|
import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
|
||||||
|
import { createSessionManager } from '~/server/utils/session';
|
||||||
import type { EventAttendanceRequest } from '~/utils/types';
|
import type { EventAttendanceRequest } from '~/utils/types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
|
@ -14,8 +15,11 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user session
|
// Get user session using the proper SessionManager
|
||||||
const session = await getUserSession(event);
|
const sessionManager = createSessionManager();
|
||||||
|
const cookieHeader = getHeader(event, 'cookie');
|
||||||
|
const session = sessionManager.getSession(cookieHeader);
|
||||||
|
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
|
|
@ -63,7 +67,7 @@ export default defineEventHandler(async (event) => {
|
||||||
message: `Attendance ${body.attended ? 'marked' : 'unmarked'} successfully`
|
message: `Attendance ${body.attended ? 'marked' : 'unmarked'} successfully`
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Error updating attendance:', error);
|
console.error('Error updating attendance:', error);
|
||||||
|
|
||||||
if (error.statusCode) {
|
if (error.statusCode) {
|
||||||
|
|
@ -76,20 +80,3 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper function
|
|
||||||
async function getUserSession(event: any) {
|
|
||||||
try {
|
|
||||||
const sessionCookie = getCookie(event, 'session') || getHeader(event, 'authorization');
|
|
||||||
if (!sessionCookie) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: {
|
|
||||||
id: 'user-id',
|
|
||||||
tier: 'board' // Replace with actual session logic
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// server/api/events/[id]/rsvp.post.ts
|
// server/api/events/[id]/rsvp.post.ts
|
||||||
import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
|
import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
|
||||||
import { getMemberByKeycloakId } from '~/server/utils/nocodb';
|
import { getMemberByKeycloakId } from '~/server/utils/nocodb';
|
||||||
|
import { createSessionManager } from '~/server/utils/session';
|
||||||
import type { EventRSVPRequest } from '~/utils/types';
|
import type { EventRSVPRequest } from '~/utils/types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
|
@ -15,8 +16,11 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user session
|
// Get user session using the proper SessionManager
|
||||||
const session = await getUserSession(event);
|
const sessionManager = createSessionManager();
|
||||||
|
const cookieHeader = getHeader(event, 'cookie');
|
||||||
|
const session = sessionManager.getSession(cookieHeader);
|
||||||
|
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
|
|
@ -66,7 +70,7 @@ export default defineEventHandler(async (event) => {
|
||||||
const paymentReference = generatePaymentReference(member.member_id || member.Id);
|
const paymentReference = generatePaymentReference(member.member_id || member.Id);
|
||||||
|
|
||||||
// Determine pricing and payment status
|
// Determine pricing and payment status
|
||||||
let paymentStatus = 'not_required';
|
let paymentStatus: 'pending' | 'not_required' | 'paid' | 'overdue' = 'not_required';
|
||||||
let isMemberPricing = 'false';
|
let isMemberPricing = 'false';
|
||||||
|
|
||||||
if (eventDetails.is_paid === 'true' && body.rsvp_status === 'confirmed') {
|
if (eventDetails.is_paid === 'true' && body.rsvp_status === 'confirmed') {
|
||||||
|
|
@ -96,7 +100,7 @@ export default defineEventHandler(async (event) => {
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
const newRSVP = await eventsClient.create(rsvpData);
|
const newRSVP = await eventsClient.createRSVP(rsvpData);
|
||||||
|
|
||||||
// Include payment information in response for paid events
|
// Include payment information in response for paid events
|
||||||
let responseData: any = newRSVP;
|
let responseData: any = newRSVP;
|
||||||
|
|
@ -121,12 +125,12 @@ export default defineEventHandler(async (event) => {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: responseData,
|
data: responseData,
|
||||||
message: body.rsvp_status === 'waitlist' ?
|
message: (body.rsvp_status as string) === 'waitlist' ?
|
||||||
'Added to waitlist - event is full' :
|
'Added to waitlist - event is full' :
|
||||||
'RSVP submitted successfully'
|
'RSVP submitted successfully'
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Error processing RSVP:', error);
|
console.error('Error processing RSVP:', error);
|
||||||
|
|
||||||
if (error.statusCode) {
|
if (error.statusCode) {
|
||||||
|
|
@ -141,19 +145,6 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
async function getUserSession(event: any) {
|
|
||||||
try {
|
|
||||||
// For now, return a mock session - this should integrate with your actual auth system
|
|
||||||
return {
|
|
||||||
user: {
|
|
||||||
id: 'mock-keycloak-id',
|
|
||||||
tier: 'user'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generatePaymentReference(memberId: string): string {
|
function generatePaymentReference(memberId: string): string {
|
||||||
const date = new Date().toISOString().split('T')[0];
|
const date = new Date().toISOString().split('T')[0];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// server/api/events/index.get.ts
|
// server/api/events/index.get.ts
|
||||||
import { createNocoDBEventsClient, transformEventForCalendar } from '~/server/utils/nocodb-events';
|
import { createNocoDBEventsClient, transformEventForCalendar } from '~/server/utils/nocodb-events';
|
||||||
|
import { createSessionManager } from '~/server/utils/session';
|
||||||
import type { EventFilters } from '~/utils/types';
|
import type { EventFilters } from '~/utils/types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
|
@ -10,8 +11,11 @@ export default defineEventHandler(async (event) => {
|
||||||
calendar_format?: string
|
calendar_format?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get user session for role-based filtering
|
// Get user session using the proper SessionManager
|
||||||
const session = await getUserSession(event);
|
const sessionManager = createSessionManager();
|
||||||
|
const cookieHeader = getHeader(event, 'cookie');
|
||||||
|
const session = sessionManager.getSession(cookieHeader);
|
||||||
|
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
|
|
@ -59,32 +63,17 @@ export default defineEventHandler(async (event) => {
|
||||||
pagination: response.PageInfo
|
pagination: response.PageInfo
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching events:', error);
|
console.error('Error fetching events:', error);
|
||||||
|
|
||||||
|
// Re-throw authentication errors as-is
|
||||||
|
if (error.statusCode === 401) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: 'Failed to fetch events'
|
statusMessage: 'Failed to fetch events'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper function to get user session (you may need to adjust this based on your auth implementation)
|
|
||||||
async function getUserSession(event: any) {
|
|
||||||
// This should be replaced with your actual session retrieval logic
|
|
||||||
// For now, assuming you have a session utility similar to your auth system
|
|
||||||
try {
|
|
||||||
const sessionCookie = getCookie(event, 'session') || getHeader(event, 'authorization');
|
|
||||||
if (!sessionCookie) return null;
|
|
||||||
|
|
||||||
// Decode session - adjust based on your session implementation
|
|
||||||
// This is a placeholder that should be replaced with your actual session logic
|
|
||||||
return {
|
|
||||||
user: {
|
|
||||||
id: 'user-id', // This should come from your session
|
|
||||||
tier: 'user' // This should come from your session
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
// server/api/events/index.post.ts
|
// server/api/events/index.post.ts
|
||||||
import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
|
import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
|
||||||
|
import { createSessionManager } from '~/server/utils/session';
|
||||||
import type { EventCreateRequest } from '~/utils/types';
|
import type { EventCreateRequest } from '~/utils/types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const body = await readBody(event) as EventCreateRequest;
|
const body = await readBody(event) as EventCreateRequest;
|
||||||
|
|
||||||
// Get user session for authentication and authorization
|
// Get user session using the proper SessionManager
|
||||||
const session = await getUserSession(event);
|
const sessionManager = createSessionManager();
|
||||||
|
const cookieHeader = getHeader(event, 'cookie');
|
||||||
|
const session = sessionManager.getSession(cookieHeader);
|
||||||
|
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
|
|
@ -74,7 +78,7 @@ export default defineEventHandler(async (event) => {
|
||||||
const eventData = {
|
const eventData = {
|
||||||
title: body.title.trim(),
|
title: body.title.trim(),
|
||||||
description: body.description?.trim() || '',
|
description: body.description?.trim() || '',
|
||||||
event_type: body.event_type,
|
event_type: body.event_type as 'meeting' | 'social' | 'fundraiser' | 'workshop' | 'board-only',
|
||||||
start_datetime: body.start_datetime,
|
start_datetime: body.start_datetime,
|
||||||
end_datetime: body.end_datetime,
|
end_datetime: body.end_datetime,
|
||||||
location: body.location?.trim() || '',
|
location: body.location?.trim() || '',
|
||||||
|
|
@ -85,10 +89,10 @@ export default defineEventHandler(async (event) => {
|
||||||
cost_members: body.cost_members || '',
|
cost_members: body.cost_members || '',
|
||||||
cost_non_members: body.cost_non_members || '',
|
cost_non_members: body.cost_non_members || '',
|
||||||
member_pricing_enabled: body.member_pricing_enabled || 'true',
|
member_pricing_enabled: body.member_pricing_enabled || 'true',
|
||||||
visibility: body.visibility,
|
visibility: body.visibility as 'public' | 'board-only' | 'admin-only',
|
||||||
status: body.status || 'active',
|
status: (body.status as 'active' | 'cancelled' | 'completed' | 'draft') || 'active',
|
||||||
creator: session.user.id,
|
creator: session.user.id,
|
||||||
current_attendees: '0'
|
current_attendees: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the event
|
// Create the event
|
||||||
|
|
@ -100,7 +104,7 @@ export default defineEventHandler(async (event) => {
|
||||||
message: 'Event created successfully'
|
message: 'Event created successfully'
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Error creating event:', error);
|
console.error('Error creating event:', error);
|
||||||
|
|
||||||
// Re-throw createError instances
|
// Re-throw createError instances
|
||||||
|
|
@ -114,20 +118,3 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper function to get user session (same as in index.get.ts)
|
|
||||||
async function getUserSession(event: any) {
|
|
||||||
try {
|
|
||||||
const sessionCookie = getCookie(event, 'session') || getHeader(event, 'authorization');
|
|
||||||
if (!sessionCookie) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: {
|
|
||||||
id: 'user-id', // Replace with actual session logic
|
|
||||||
tier: 'board' // Replace with actual session logic
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,11 @@ export function getEffectiveNocoDBConfig(): EffectiveNocoDB {
|
||||||
url: runtimeConfig.nocodb?.url || 'https://database.monacousa.org',
|
url: runtimeConfig.nocodb?.url || 'https://database.monacousa.org',
|
||||||
token: runtimeConfig.nocodb?.token || '',
|
token: runtimeConfig.nocodb?.token || '',
|
||||||
baseId: runtimeConfig.nocodb?.baseId || '',
|
baseId: runtimeConfig.nocodb?.baseId || '',
|
||||||
tables: { members: 'members-table-id' } // Default table mapping
|
tables: {
|
||||||
|
members: 'members-table-id',
|
||||||
|
events: (runtimeConfig.nocodb as any)?.eventsTableId || '',
|
||||||
|
rsvps: (runtimeConfig.nocodb as any)?.rsvpTableId || ''
|
||||||
|
} // Default table mapping
|
||||||
};
|
};
|
||||||
|
|
||||||
// Override with file configuration if available
|
// Override with file configuration if available
|
||||||
|
|
@ -306,7 +310,11 @@ export async function getCurrentConfig(): Promise<NocoDBSettings> {
|
||||||
url: config.nocodb.url || runtimeConfig.nocodb?.url || 'https://database.monacousa.org',
|
url: config.nocodb.url || runtimeConfig.nocodb?.url || 'https://database.monacousa.org',
|
||||||
apiKey: config.nocodb.apiKey ? '••••••••••••••••' : '',
|
apiKey: config.nocodb.apiKey ? '••••••••••••••••' : '',
|
||||||
baseId: config.nocodb.baseId || runtimeConfig.nocodb?.baseId || '',
|
baseId: config.nocodb.baseId || runtimeConfig.nocodb?.baseId || '',
|
||||||
tables: config.nocodb.tables || { members: 'members-table-id' }
|
tables: config.nocodb.tables || {
|
||||||
|
members: 'members-table-id',
|
||||||
|
events: '',
|
||||||
|
rsvps: ''
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,7 +323,11 @@ export async function getCurrentConfig(): Promise<NocoDBSettings> {
|
||||||
url: runtimeConfig.nocodb?.url || 'https://database.monacousa.org',
|
url: runtimeConfig.nocodb?.url || 'https://database.monacousa.org',
|
||||||
apiKey: runtimeConfig.nocodb?.token ? '••••••••••••••••' : '',
|
apiKey: runtimeConfig.nocodb?.token ? '••••••••••••••••' : '',
|
||||||
baseId: runtimeConfig.nocodb?.baseId || '',
|
baseId: runtimeConfig.nocodb?.baseId || '',
|
||||||
tables: { members: 'members-table-id' }
|
tables: {
|
||||||
|
members: 'members-table-id',
|
||||||
|
events: '',
|
||||||
|
rsvps: ''
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,7 +340,7 @@ export async function saveRecaptchaConfig(config: { siteKey: string; secretKey:
|
||||||
await createBackup();
|
await createBackup();
|
||||||
|
|
||||||
const currentConfig = configCache || await loadAdminConfig() || {
|
const currentConfig = configCache || await loadAdminConfig() || {
|
||||||
nocodb: { url: '', apiKey: '', baseId: '', tables: {} },
|
nocodb: { url: '', apiKey: '', baseId: '', tables: { members: '', events: '', rsvps: '' } },
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
updatedBy: 'system'
|
updatedBy: 'system'
|
||||||
};
|
};
|
||||||
|
|
@ -374,7 +386,7 @@ export async function saveRegistrationConfig(config: { membershipFee: number; ib
|
||||||
await createBackup();
|
await createBackup();
|
||||||
|
|
||||||
const currentConfig = configCache || await loadAdminConfig() || {
|
const currentConfig = configCache || await loadAdminConfig() || {
|
||||||
nocodb: { url: '', apiKey: '', baseId: '', tables: {} },
|
nocodb: { url: '', apiKey: '', baseId: '', tables: { members: '', events: '', rsvps: '' } },
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
updatedBy: 'system'
|
updatedBy: 'system'
|
||||||
};
|
};
|
||||||
|
|
@ -430,7 +442,7 @@ export async function saveSMTPConfig(config: SMTPConfig, updatedBy: string): Pro
|
||||||
await createBackup();
|
await createBackup();
|
||||||
|
|
||||||
const currentConfig = configCache || await loadAdminConfig() || {
|
const currentConfig = configCache || await loadAdminConfig() || {
|
||||||
nocodb: { url: '', apiKey: '', baseId: '', tables: {} },
|
nocodb: { url: '', apiKey: '', baseId: '', tables: { members: '', events: '', rsvps: '' } },
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
updatedBy: 'system'
|
updatedBy: 'system'
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,15 @@ import type { Event, EventRSVP, EventsResponse, EventFilters } from '~/utils/typ
|
||||||
* Provides CRUD operations and specialized queries for events and RSVPs
|
* Provides CRUD operations and specialized queries for events and RSVPs
|
||||||
*/
|
*/
|
||||||
export function createNocoDBEventsClient() {
|
export function createNocoDBEventsClient() {
|
||||||
const config = useRuntimeConfig();
|
// Get effective configuration from admin config system
|
||||||
|
const { getEffectiveNocoDBConfig } = require('./admin-config');
|
||||||
|
const effectiveConfig = getEffectiveNocoDBConfig();
|
||||||
|
|
||||||
const baseUrl = config.nocodb.url;
|
const baseUrl = effectiveConfig.url;
|
||||||
const token = config.nocodb.token;
|
const token = effectiveConfig.token;
|
||||||
const eventsBaseId = config.nocodb.eventsBaseId;
|
const eventsBaseId = effectiveConfig.baseId;
|
||||||
const eventsTableId = config.nocodb.eventsTableId || 'events'; // fallback to table name
|
const eventsTableId = effectiveConfig.tables.events || 'events';
|
||||||
|
const rsvpTableId = effectiveConfig.tables.rsvps || 'event_rsvps';
|
||||||
|
|
||||||
if (!baseUrl || !token || !eventsBaseId) {
|
if (!baseUrl || !token || !eventsBaseId) {
|
||||||
throw new Error('Events NocoDB configuration is incomplete. Please check environment variables.');
|
throw new Error('Events NocoDB configuration is incomplete. Please check environment variables.');
|
||||||
|
|
@ -150,12 +153,12 @@ export function createNocoDBEventsClient() {
|
||||||
* Create an RSVP record for an event
|
* Create an RSVP record for an event
|
||||||
*/
|
*/
|
||||||
async createRSVP(rsvpData: Partial<EventRSVP>) {
|
async createRSVP(rsvpData: Partial<EventRSVP>) {
|
||||||
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}`;
|
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${rsvpTableId}`;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
...rsvpData,
|
...rsvpData,
|
||||||
created_at: new Date().toISOString(),
|
created_time: new Date().toISOString(),
|
||||||
updated_at: new Date().toISOString()
|
updated_time: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
return await $fetch<EventRSVP>(url, {
|
return await $fetch<EventRSVP>(url, {
|
||||||
|
|
@ -171,9 +174,9 @@ export function createNocoDBEventsClient() {
|
||||||
async findEventRSVPs(eventId: string) {
|
async findEventRSVPs(eventId: string) {
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
queryParams.set('where', `(event_id = '${eventId}')`);
|
queryParams.set('where', `(event_id = '${eventId}')`);
|
||||||
queryParams.set('sort', 'created_at');
|
queryParams.set('sort', 'created_time');
|
||||||
|
|
||||||
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${queryParams.toString()}`;
|
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${rsvpTableId}?${queryParams.toString()}`;
|
||||||
|
|
||||||
return await $fetch(url, {
|
return await $fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
@ -189,9 +192,9 @@ export function createNocoDBEventsClient() {
|
||||||
queryParams.set('where', `(event_id = '${eventId}' AND member_id = '${memberId}')`);
|
queryParams.set('where', `(event_id = '${eventId}' AND member_id = '${memberId}')`);
|
||||||
queryParams.set('limit', '1');
|
queryParams.set('limit', '1');
|
||||||
|
|
||||||
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${queryParams.toString()}`;
|
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${rsvpTableId}?${queryParams.toString()}`;
|
||||||
|
|
||||||
const response = await $fetch(url, {
|
const response = await $fetch<{ list?: EventRSVP[]; PageInfo?: any }>(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
|
|
@ -203,11 +206,11 @@ export function createNocoDBEventsClient() {
|
||||||
* Update an RSVP record
|
* Update an RSVP record
|
||||||
*/
|
*/
|
||||||
async updateRSVP(id: string, rsvpData: Partial<EventRSVP>) {
|
async updateRSVP(id: string, rsvpData: Partial<EventRSVP>) {
|
||||||
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`;
|
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${rsvpTableId}/${id}`;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
...rsvpData,
|
...rsvpData,
|
||||||
updated_at: new Date().toISOString()
|
updated_time: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
return await $fetch<EventRSVP>(url, {
|
return await $fetch<EventRSVP>(url, {
|
||||||
|
|
@ -238,7 +241,7 @@ export function createNocoDBEventsClient() {
|
||||||
*/
|
*/
|
||||||
async findUserEvents(memberId: string, filters?: EventFilters) {
|
async findUserEvents(memberId: string, filters?: EventFilters) {
|
||||||
// First get all visible events
|
// First get all visible events
|
||||||
const events = await this.findAll(filters);
|
const events = await this.findAll(filters) as { list?: Event[]; PageInfo?: any };
|
||||||
|
|
||||||
if (!events.list || events.list.length === 0) {
|
if (!events.list || events.list.length === 0) {
|
||||||
return { list: [], PageInfo: events.PageInfo };
|
return { list: [], PageInfo: events.PageInfo };
|
||||||
|
|
@ -247,11 +250,11 @@ export function createNocoDBEventsClient() {
|
||||||
// Get user's RSVPs for these events
|
// Get user's RSVPs for these events
|
||||||
const eventIds = events.list.map((e: Event) => e.id);
|
const eventIds = events.list.map((e: Event) => e.id);
|
||||||
const rsvpQueryParams = new URLSearchParams();
|
const rsvpQueryParams = new URLSearchParams();
|
||||||
rsvpQueryParams.set('where', `(member_id = '${memberId}' AND event_id IN (${eventIds.map(id => `'${id}'`).join(',')}))`);
|
rsvpQueryParams.set('where', `(member_id = '${memberId}' AND event_id IN (${eventIds.map((id: string) => `'${id}'`).join(',')}))`);
|
||||||
|
|
||||||
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${rsvpQueryParams.toString()}`;
|
const rsvpUrl = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${rsvpTableId}?${rsvpQueryParams.toString()}`;
|
||||||
|
|
||||||
const rsvps = await $fetch(url, {
|
const rsvps = await $fetch<{ list?: EventRSVP[]; PageInfo?: any }>(rsvpUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
|
|
@ -294,7 +297,7 @@ export function createNocoDBEventsClient() {
|
||||||
if (!event.max_attendees) return false; // Unlimited capacity
|
if (!event.max_attendees) return false; // Unlimited capacity
|
||||||
|
|
||||||
const maxAttendees = parseInt(event.max_attendees);
|
const maxAttendees = parseInt(event.max_attendees);
|
||||||
const currentAttendees = event.current_attendees || 0;
|
const currentAttendees = parseInt(String(event.current_attendees || 0));
|
||||||
|
|
||||||
return currentAttendees >= maxAttendees;
|
return currentAttendees >= maxAttendees;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,12 @@ export interface NocoDBSettings {
|
||||||
url: string;
|
url: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
baseId: string;
|
baseId: string;
|
||||||
tables: { [tableName: string]: string }; // e.g., { "members": "m2sri3jqfqutiy5", "events": "evt123abc", ... }
|
tables: {
|
||||||
|
members: string;
|
||||||
|
events: string;
|
||||||
|
rsvps: string;
|
||||||
|
[tableName: string]: string;
|
||||||
|
}; // e.g., { "members": "m2sri3jqfqutiy5", "events": "evt123abc", "rsvps": "rsvp456def" }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemberResponse {
|
export interface MemberResponse {
|
||||||
|
|
@ -452,11 +457,11 @@ export interface Event {
|
||||||
visibility: 'public' | 'board-only' | 'admin-only';
|
visibility: 'public' | 'board-only' | 'admin-only';
|
||||||
status: 'active' | 'cancelled' | 'completed' | 'draft';
|
status: 'active' | 'cancelled' | 'completed' | 'draft';
|
||||||
creator: string; // member_id who created event
|
creator: string; // member_id who created event
|
||||||
created_at: string;
|
created_time: string; // Updated to match database schema
|
||||||
updated_at: string;
|
updated_time: string; // Updated to match database schema
|
||||||
|
|
||||||
// Computed fields
|
// Computed fields
|
||||||
current_attendees?: number;
|
current_attendees?: string; // Changed to string to match database
|
||||||
user_rsvp?: EventRSVP;
|
user_rsvp?: EventRSVP;
|
||||||
attendee_list?: EventRSVP[];
|
attendee_list?: EventRSVP[];
|
||||||
}
|
}
|
||||||
|
|
@ -470,8 +475,8 @@ export interface EventRSVP {
|
||||||
payment_reference: string; // EVT-{member_id}-{date}
|
payment_reference: string; // EVT-{member_id}-{date}
|
||||||
attended: string; // 'true' or 'false' as string
|
attended: string; // 'true' or 'false' as string
|
||||||
rsvp_notes?: string;
|
rsvp_notes?: string;
|
||||||
created_at: string;
|
created_time: string; // Updated to match database schema
|
||||||
updated_at: string;
|
updated_time: string; // Updated to match database schema
|
||||||
|
|
||||||
// Computed fields
|
// Computed fields
|
||||||
member_details?: Member;
|
member_details?: Member;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue