From 28fa779dae617021e900e28ec56653336bbcd2a6 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 8 Aug 2025 13:50:01 +0200 Subject: [PATCH] fixes and cleanup --- COMPREHENSIVE_FIX_SUMMARY.md | 285 -- FINAL_LOGIN_SOLUTION_CLEAN.md | 201 -- .../MONACOUSA_PORTAL_IMPLEMENTATION.md | 2352 ----------------- KEYCLOAK_CUSTOM_LOGIN_IMPLEMENTATION_GUIDE.md | 1482 ----------- LOGIN_LOOP_FIX_FINAL.md | 134 - MEMBER_CREATION_FIX_SUMMARY.md | 134 - MEMBER_UNDEFINED_DISPLAY_FIX_COMPREHENSIVE.md | 212 -- PASSWORD_RESET_FIX_SUMMARY.md | 110 - REDIRECT_LOOP_SOLUTION_FINAL.md | 222 -- SNAKE_CASE_FIELD_MIGRATION_GUIDE.md | 187 -- components/AddMemberDialog.vue | 56 +- components/EnhancedPhoneInput.vue | 147 -- components/MultipleNationalityInput.vue | 8 +- components/PhoneInputDemo.vue | 164 -- components/PhoneInputWrapper.vue | 1 - nocodb-implementation-guide.md | 1269 --------- nuxt.config.ts | 1 - package.json | 6 +- pages/test-phone.vue | 87 - 19 files changed, 34 insertions(+), 7024 deletions(-) delete mode 100644 COMPREHENSIVE_FIX_SUMMARY.md delete mode 100644 FINAL_LOGIN_SOLUTION_CLEAN.md delete mode 100644 Implementation_Plan_Initial/MONACOUSA_PORTAL_IMPLEMENTATION.md delete mode 100644 KEYCLOAK_CUSTOM_LOGIN_IMPLEMENTATION_GUIDE.md delete mode 100644 LOGIN_LOOP_FIX_FINAL.md delete mode 100644 MEMBER_CREATION_FIX_SUMMARY.md delete mode 100644 MEMBER_UNDEFINED_DISPLAY_FIX_COMPREHENSIVE.md delete mode 100644 PASSWORD_RESET_FIX_SUMMARY.md delete mode 100644 REDIRECT_LOOP_SOLUTION_FINAL.md delete mode 100644 SNAKE_CASE_FIELD_MIGRATION_GUIDE.md delete mode 100644 components/EnhancedPhoneInput.vue delete mode 100644 components/PhoneInputDemo.vue delete mode 100644 nocodb-implementation-guide.md delete mode 100644 pages/test-phone.vue diff --git a/COMPREHENSIVE_FIX_SUMMARY.md b/COMPREHENSIVE_FIX_SUMMARY.md deleted file mode 100644 index 5719c9f..0000000 --- a/COMPREHENSIVE_FIX_SUMMARY.md +++ /dev/null @@ -1,285 +0,0 @@ -# ๐ŸŽฏ MonacoUSA Portal - Comprehensive Fix Summary - -## ๐Ÿ“‹ **Implementation Overview** - -All 6 priority issues have been successfully addressed with comprehensive code changes and enhancements. This document outlines the specific fixes implemented to resolve authentication problems, mobile compatibility issues, system metrics display, firstName extraction, and PWA styling. - ---- - -## โœ… **Priority 1: Authentication Cookie Settings** (Complexity: 7) -**Status: COMPLETED** - -### ๐Ÿ”ง **Changes Made** -**File:** `server/api/auth/direct-login.post.ts` - -- **Cookie Configuration**: Changed `sameSite` from `'none'` to `'lax'` for mobile browser compatibility -- **Environment-Specific Security**: Set `secure: process.env.NODE_ENV === 'production'` instead of always `true` -- **Enhanced Logging**: Added detailed cookie setting logs for debugging - -### ๐Ÿ“ **Key Code Changes** -```typescript -setCookie(event, 'monacousa-session', sessionId, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', // Environment-specific - sameSite: 'lax', // Changed from 'none' for mobile compatibility - maxAge, - path: '/', -}); -``` - -### ๐ŸŽฏ **Impact** -- **Mobile browsers can now properly handle authentication cookies** -- **Resolves the root cause of 403 errors on admin APIs** -- **Should fix system metrics and firstName display issues** - ---- - -## โœ… **Priority 2: Mobile Login Refresh Loop** (Complexity: 8) -**Status: COMPLETED** - -### ๐Ÿ”ง **Changes Made** -**File:** `pages/login.vue` - -- **Mobile Detection**: Integrated comprehensive mobile device detection -- **Enhanced Redirect Logic**: Added mobile-specific redirect handling with delays -- **Debug Integration**: Added mobile login debugging capabilities - -### ๐Ÿ“ **Key Code Changes** -```typescript -// Mobile-specific handling for already authenticated users -if (isMobileDevice()) { - console.log('๐Ÿ“ฑ Mobile browser detected, using delayed redirect'); - debugMobileLogin('Already authenticated redirect'); - setTimeout(() => { - window.location.href = redirectUrl; - }, 100); -} else { - await navigateTo(redirectUrl); -} -``` - -### ๐ŸŽฏ **Impact** -- **Eliminates refresh loops on mobile browsers** -- **Provides better mobile user experience** -- **Enhanced debugging for mobile-specific issues** - ---- - -## โœ… **Priority 3: System Metrics Display Issues** (Complexity: 6) -**Status: COMPLETED** - -### ๐Ÿ”ง **Changes Made** -**File:** `server/utils/system-metrics.ts` - -- **Enhanced Error Handling**: Added `Promise.allSettled` to handle individual metric failures -- **Fallback Mechanisms**: Graceful degradation when systeminformation library fails -- **Better Logging**: Individual error logging for each metric type - -### ๐Ÿ“ **Key Code Changes** -```typescript -// Individual error handling for each metric -const results = await Promise.allSettled([ - si.cpu(), - si.mem(), - si.fsSize(), - si.currentLoad(), - si.processes(), - si.time() -]); - -// Extract data with fallbacks for failed promises -const cpuData = results[0].status === 'fulfilled' ? results[0].value : { cores: 0, brand: 'Unknown' }; -``` - -### ๐ŸŽฏ **Impact** -- **System metrics will display properly once authentication is fixed** -- **Robust error handling for production environments** -- **Better debugging and monitoring capabilities** - ---- - -## โœ… **Priority 4: firstName Display Issues** (Complexity: 4) -**Status: COMPLETED** - -### ๐Ÿ”ง **Changes Made** -**File:** `server/api/auth/session.get.ts` - -- **Enhanced User Data Logging**: Added comprehensive logging of user data fields -- **Debug Information**: Detailed firstName, lastName, and name field tracking - -### ๐Ÿ“ **Key Code Changes** -```typescript -console.log('๐Ÿ‘ค User data:', { - id: session.user.id, - email: session.user.email, - name: session.user.name, - firstName: session.user.firstName, - lastName: session.user.lastName, - username: session.user.username -}); -``` - -### ๐ŸŽฏ **Impact** -- **Better tracking of firstName data flow from Keycloak** -- **Enhanced debugging for user data extraction** -- **Should resolve "User!" display issue once authentication is fixed** - ---- - -## โœ… **Priority 5: PWA Banner Solid Color** (Complexity: 3) -**Status: COMPLETED** - -### ๐Ÿ”ง **Changes Made** -**File:** `components/PWAInstallBanner.vue` - -- **Solid Background**: Changed from gradient to solid MonacoUSA red (#a31515) -- **Removed Dynamic Colors**: Eliminated `color="primary"` attribute -- **Custom Styling**: Added explicit background color with override - -### ๐Ÿ“ **Key Code Changes** -```css -.pwa-install-banner { - background: #a31515 !important; /* Solid MonacoUSA red */ - background-image: none !important; /* Remove any gradients */ -} -``` - -### ๐ŸŽฏ **Impact** -- **Consistent MonacoUSA brand color across PWA banner** -- **Clean, professional appearance** -- **Better visual consistency with overall theme** - ---- - -## โœ… **Priority 6: Mobile Detection & Debugging** (Complexity: 5) -**Status: COMPLETED** - -### ๐Ÿ”ง **Changes Made** -**Files:** -- `utils/mobile-utils.ts` (new file) -- `pages/login.vue` (integration) - -- **Comprehensive Device Detection**: Browser, OS, version detection -- **Debug Utilities**: Mobile login debugging, network analysis, PWA capabilities -- **Compatibility Checks**: Known mobile browser issues identification - -### ๐Ÿ“ **Key Code Changes** -```typescript -// Mobile debugging integration -const { isMobileDevice, debugMobileLogin, runMobileDiagnostics } = await import('~/utils/mobile-utils'); - -// Enhanced mobile debugging -debugMobileLogin('Already authenticated redirect'); -``` - -### ๐ŸŽฏ **Impact** -- **Comprehensive mobile debugging capabilities** -- **Better understanding of mobile browser compatibility** -- **Enhanced troubleshooting for mobile-specific issues** - ---- - -## ๐Ÿ” **Testing & Validation** - -### **Immediate Testing Steps** -1. **Authentication Flow**: Test login on both desktop and mobile browsers -2. **Cookie Functionality**: Verify cookies are set properly with new sameSite settings -3. **System Metrics**: Check admin dashboard for real system data display -4. **firstName Display**: Confirm user names appear correctly instead of "User!" -5. **PWA Banner**: Verify solid MonacoUSA red background -6. **Mobile Experience**: Test login flow on iOS Safari and Chrome Mobile - -### **Expected Results** -- โœ… Mobile users can login without refresh loops -- โœ… Admin dashboard shows real system metrics (CPU, memory, disk) -- โœ… User names display correctly (firstName from Keycloak data) -- โœ… PWA banner has solid MonacoUSA red background (#a31515) -- โœ… Enhanced debugging information for mobile issues - ---- - -## ๐Ÿ“ˆ **Performance & Security Improvements** - -### **Security Enhancements** -- **Environment-Appropriate Cookie Security**: Only secure cookies in production -- **Better SameSite Policy**: `lax` setting provides good security with mobile compatibility -- **Enhanced Session Debugging**: Better tracking of authentication flow - -### **Performance Optimizations** -- **System Metrics Caching**: 30-second cache with graceful fallbacks -- **Mobile-Specific Optimizations**: Reduced redirects and better mobile handling -- **Efficient Error Handling**: Individual metric failures don't crash entire system - ---- - -## ๐Ÿš€ **Deployment Considerations** - -### **Environment Variables** -Ensure these are properly set in production: -- `NODE_ENV=production` (for secure cookie handling) -- All Keycloak configuration variables -- NocoDB and MinIO credentials - -### **Testing Checklist** -- [ ] Desktop login flow works -- [ ] Mobile login flow works (iOS Safari, Chrome Mobile) -- [ ] Admin dashboard displays system metrics -- [ ] User names show correctly -- [ ] PWA banner appears with solid color -- [ ] No console errors in mobile browsers - ---- - -## ๐Ÿ”ง **Technical Architecture** - -### **Cookie Strategy** -``` -Development: secure=false, sameSite=lax -Production: secure=true, sameSite=lax -``` - -### **Mobile Compatibility Matrix** -- โœ… **iOS Safari 14+**: Full compatibility with SameSite=lax -- โœ… **Chrome Mobile 80+**: Full SameSite support -- โœ… **Android WebView**: Compatible with lax policy -- โš ๏ธ **Legacy Browsers**: May need fallback handling - -### **Error Handling Strategy** -- **Authentication**: Graceful fallbacks with user-friendly messages -- **System Metrics**: Individual metric failures with fallback data -- **Mobile Detection**: Progressive enhancement with desktop fallbacks - ---- - -## ๐Ÿ“Š **Success Metrics** - -### **Before Fixes** -- โŒ Mobile login refresh loops -- โŒ System metrics showing zeros -- โŒ Username showing "User!" -- โŒ PWA banner gradient background -- โŒ Limited mobile debugging - -### **After Fixes** -- โœ… Smooth mobile login experience -- โœ… Real system metrics display -- โœ… Proper firstName extraction -- โœ… Solid MonacoUSA brand color -- โœ… Comprehensive mobile debugging - ---- - -## ๐ŸŽฏ **Final Summary** - -All **6 priority issues** have been comprehensively addressed with: - -1. **๐Ÿช Cookie compatibility** for mobile browsers -2. **๐Ÿ“ฑ Mobile-specific** redirect handling -3. **๐Ÿ“Š Robust system metrics** with fallbacks -4. **๐Ÿ‘ค Enhanced user data** debugging -5. **๐ŸŽจ Consistent PWA styling** with brand colors -6. **๐Ÿ”ง Comprehensive mobile** debugging utilities - -The fixes target the **root authentication issues** while providing **enhanced mobile support** and **better debugging capabilities** for ongoing maintenance and troubleshooting. - -**Next Steps**: Deploy changes and test the complete authentication flow on both desktop and mobile devices to validate all fixes are working as expected. diff --git a/FINAL_LOGIN_SOLUTION_CLEAN.md b/FINAL_LOGIN_SOLUTION_CLEAN.md deleted file mode 100644 index 3ad9e47..0000000 --- a/FINAL_LOGIN_SOLUTION_CLEAN.md +++ /dev/null @@ -1,201 +0,0 @@ -# ๐ŸŽฏ Final Login Solution - Clean & Simple - -## ๐Ÿšจ **Problems Solved** - -### โŒ **Before Fix** -- **Desktop**: White screen after login attempt -- **Mobile**: Endless login loop in iOS Safari -- **Server**: Session API spam (50+ calls per 30 seconds) - -### โœ… **After Fix** -- **Desktop**: Clean login flow with proper redirects -- **Mobile**: No more loops, standard navigation works -- **Server**: Simple session checks, no spam - -## ๐Ÿ”ง **Root Cause Analysis** - -Using sequential thinking MCP, I identified two critical issues: - -1. **White Screen**: `await checkAuth(true)` at top level of login page broke SSR/hydration -2. **Mobile Loops**: Complex throttling mechanism prevented proper auth flow - -## โœ… **Complete Solution Implemented** - -### **1. Simplified checkAuth Function** -```typescript -// composables/useAuth.ts -const checkAuth = async () => { - try { - console.log('๐Ÿ”„ Performing session check...'); - - const response = await $fetch<{ - authenticated: boolean; - user: User | null; - }>('/api/auth/session'); - - if (response.authenticated && response.user) { - user.value = response.user; - return true; - } else { - user.value = null; - return false; - } - } catch (err) { - console.error('Auth check error:', err); - user.value = null; - return false; - } -}; -``` -**Changes**: -- โŒ Removed ALL throttling logic -- โŒ Removed force parameter -- โœ… Simple, reliable session checks - -### **2. Fixed Login Page** -```typescript -// pages/login.vue -onMounted(async () => { - // Check if user is already authenticated (client-side only) - const isAuthenticated = await checkAuth(); - if (isAuthenticated && user.value) { - console.log('๐Ÿ”„ User already authenticated, redirecting to dashboard'); - await navigateTo('/dashboard'); - return; - } - - // Auto-focus username field - nextTick(() => { - const usernameField = document.querySelector('input[type="text"]') as HTMLInputElement; - if (usernameField) { - usernameField.focus(); - } - }); -}); -``` -**Changes**: -- โŒ Removed top-level async `await checkAuth(true)` -- โœ… Moved auth check to `onMounted` (client-side only) -- โœ… Standard `navigateTo()` instead of `window.location` - -### **3. Simplified Middleware** -```typescript -// middleware/auth.ts -export default defineNuxtRouteMiddleware(async (to) => { - if (to.meta.auth === false) { - return; - } - - const { isAuthenticated, checkAuth, user } = useAuth(); - - // Simple check without forcing - if (!user.value) { - await checkAuth(); - } - - if (!isAuthenticated.value) { - return navigateTo('/login'); - } -}); -``` -**Changes**: -- โŒ Removed `checkAuth(true)` forced parameter -- โœ… Simple, standard auth checks - -### **4. Clean Login Method** -```typescript -// In login method -while (!sessionSuccess && attempts < maxAttempts) { - attempts++; - console.log(`๐Ÿ”„ Session check attempt ${attempts}/${maxAttempts}`); - - sessionSuccess = await checkAuth(); // Simple check - - if (!sessionSuccess && attempts < maxAttempts) { - console.log('โณ Session not ready, waiting 500ms...'); - await new Promise(resolve => setTimeout(resolve, 500)); - } -} -``` -**Changes**: -- โŒ Removed forced checkAuth calls -- โœ… Standard retry logic - -## ๐ŸŽฏ **Key Principles Applied** - -### **1. No Over-Engineering** -- Removed complex throttling that caused more problems than it solved -- Simple auth checks work reliably across all platforms - -### **2. Proper SSR Handling** -- No async operations at top level of components -- Client-side auth checks in `onMounted` lifecycle - -### **3. Standard Nuxt Navigation** -- Use `navigateTo()` instead of `window.location` manipulations -- Let Nuxt handle routing properly - -### **4. Clean Error Handling** -- Simple try/catch blocks -- Clear logging for debugging - -## ๐Ÿ“Š **Expected Results** - -### **Desktop Experience** -- โœ… Login form appears immediately (no white screen) -- โœ… Valid credentials โ†’ redirect to dashboard -- โœ… Invalid credentials โ†’ clear error message -- โœ… Already authenticated โ†’ automatic redirect - -### **Mobile Experience (iOS Safari)** -- โœ… Smooth login flow without loops -- โœ… Standard navigation behavior -- โœ… Proper cookie handling with `sameSite: 'lax'` -- โœ… No complex mobile detection needed - -### **Server Performance** -- โœ… Reduced session API calls (from 50+ to normal levels) -- โœ… Clean session logs without spam -- โœ… Proper authentication flow - -## ๐Ÿงช **Testing Checklist** - -### **Desktop Testing** -- [ ] Login page loads without white screen -- [ ] Valid login redirects to dashboard -- [ ] Invalid login shows error -- [ ] Already authenticated users redirect automatically - -### **Mobile Testing** -- [ ] No login loops on iOS Safari -- [ ] Smooth navigation between pages -- [ ] Proper form interaction -- [ ] Correct redirect behavior - -### **Server Monitoring** -- [ ] Normal session check frequency (not 50+ per 30 seconds) -- [ ] Clean server logs -- [ ] Successful authentication flow - -## ๐ŸŽ‰ **Why This Works** - -1. **Simplicity**: Removed all complex logic that was causing issues -2. **SSR Compatibility**: Proper lifecycle management prevents hydration issues -3. **Standard Patterns**: Uses Nuxt conventions instead of custom workarounds -4. **Mobile Friendly**: Works with standard browser behavior -5. **Reliable**: Consistent behavior across all platforms - -## ๐Ÿš€ **Files Modified** - -- `composables/useAuth.ts` - Removed throttling, simplified checkAuth -- `middleware/auth.ts` - Removed forced parameters -- `pages/login.vue` - Moved auth check to onMounted, standard navigation - -## ๐Ÿ“ˆ **Success Metrics** - -- **White Screen**: โŒ โ†’ โœ… (Fixed SSR issues) -- **Mobile Loops**: โŒ โ†’ โœ… (Removed complex navigation) -- **Server Spam**: โŒ โ†’ โœ… (Removed throttling complications) -- **User Experience**: โŒ โ†’ โœ… (Clean, reliable authentication) - -The authentication system is now **simple, reliable, and works consistently** across all platforms! ๐ŸŽฏ diff --git a/Implementation_Plan_Initial/MONACOUSA_PORTAL_IMPLEMENTATION.md b/Implementation_Plan_Initial/MONACOUSA_PORTAL_IMPLEMENTATION.md deleted file mode 100644 index 1b79810..0000000 --- a/Implementation_Plan_Initial/MONACOUSA_PORTAL_IMPLEMENTATION.md +++ /dev/null @@ -1,2352 +0,0 @@ -# MonacoUSA Portal - Complete Implementation Guide - -## Overview - -This document provides step-by-step instructions to create a complete portal foundation for MonacoUSA using the same proven tech stack as the Port Nimara client portal. The portal will feature Keycloak authentication, responsive design, PWA capabilities, and a modular structure for adding custom tools. - -## Project Specifications - -- **Project Name**: monacousa-portal -- **Domain**: monacousa.org (configurable) -- **Primary Color**: #a31515 (MonacoUSA red) -- **Secondary Color**: #ffffff (white) -- **Framework**: Nuxt 3 with Vue 3 -- **UI Library**: Vuetify 3 -- **Authentication**: Keycloak (OAuth2/OIDC) -- **Database**: NocoDB -- **File Storage**: MinIO (S3-compatible) -- **Features**: PWA, Mobile-responsive, Dashboard layout - -## Prerequisites - -Before starting, ensure you have: -- Node.js 18+ installed -- Git installed -- Access to a Keycloak server -- NocoDB instance (or ability to set one up) -- MinIO instance (or ability to set one up) - -## Phase 1: Project Initialization - -### 1.1 Create New Repository - -```bash -mkdir monacousa-portal -cd monacousa-portal -git init -``` - -### 1.2 Initialize Nuxt 3 Project - -```bash -npx nuxi@latest init . -``` - -### 1.3 Install Core Dependencies - -```bash -npm install @nuxt/ui@^3.2.0 vuetify-nuxt-module@^0.18.3 @vite-pwa/nuxt@^0.10.6 motion-v@^1.6.1 -``` - -### 1.4 Install Additional Dependencies - -```bash -npm install @types/node formidable@^3.5.4 mime-types@^3.0.1 minio@^8.0.5 sharp@^0.34.2 -npm install -D @types/formidable@^3.4.5 @types/mime-types@^3.0.1 -``` - -## Phase 2: Project Structure Setup - -### 2.1 Create Directory Structure - -```bash -mkdir -p components composables layouts middleware pages/auth pages/dashboard server/api server/utils server/plugins utils docs public/icons -``` - -### 2.2 Create package.json - -```json -{ - "name": "monacousa-portal", - "type": "module", - "scripts": { - "build": "nuxt build", - "dev": "nuxt dev", - "generate": "nuxt generate", - "preview": "nuxt preview", - "postinstall": "nuxt prepare" - }, - "dependencies": { - "@nuxt/ui": "^3.2.0", - "@vite-pwa/nuxt": "^0.10.6", - "formidable": "^3.5.4", - "mime-types": "^3.0.1", - "minio": "^8.0.5", - "motion-v": "^1.6.1", - "nuxt": "^3.15.4", - "sharp": "^0.34.2", - "vue": "latest", - "vue-router": "latest", - "vuetify-nuxt-module": "^0.18.3" - }, - "devDependencies": { - "@types/formidable": "^3.4.5", - "@types/mime-types": "^3.0.1" - } -} -``` - -## Phase 3: Core Configuration - -### 3.1 Create nuxt.config.ts - -```typescript -export default defineNuxtConfig({ - ssr: false, - compatibilityDate: "2024-11-01", - devtools: { enabled: true }, - modules: ["vuetify-nuxt-module", "@vite-pwa/nuxt", "motion-v/nuxt"], - app: { - head: { - titleTemplate: "%s โ€ข MonacoUSA Portal", - title: "MonacoUSA Portal", - meta: [ - { property: "og:title", content: "MonacoUSA Portal" }, - { property: "og:image", content: "/og-image.png" }, - { name: "twitter:card", content: "summary_large_image" }, - { name: "viewport", content: "width=device-width, initial-scale=1" }, - { name: "apple-mobile-web-app-capable", content: "yes" }, - { name: "apple-mobile-web-app-status-bar-style", content: "default" }, - { name: "apple-mobile-web-app-title", content: "MonacoUSA Portal" }, - ], - htmlAttrs: { - lang: "en", - }, - }, - }, - pwa: { - registerType: 'autoUpdate', - manifest: { - name: 'MonacoUSA Portal', - short_name: 'MonacoUSA', - description: 'MonacoUSA Portal - Unified dashboard for tools and services', - theme_color: '#a31515', - background_color: '#ffffff', - display: 'standalone', - orientation: 'portrait', - start_url: '/', - scope: '/', - icons: [ - { - src: '/icons/icon-72x72.png', - sizes: '72x72', - type: 'image/png' - }, - { - src: '/icons/icon-96x96.png', - sizes: '96x96', - type: 'image/png' - }, - { - src: '/icons/icon-128x128.png', - sizes: '128x128', - type: 'image/png' - }, - { - src: '/icons/icon-144x144.png', - sizes: '144x144', - type: 'image/png' - }, - { - src: '/icons/icon-152x152.png', - sizes: '152x152', - type: 'image/png' - }, - { - src: '/icons/icon-192x192.png', - sizes: '192x192', - type: 'image/png' - }, - { - src: '/icons/icon-384x384.png', - sizes: '384x384', - type: 'image/png' - }, - { - src: '/icons/icon-512x512.png', - sizes: '512x512', - type: 'image/png' - } - ] - }, - workbox: { - navigateFallback: '/', - globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,svg,ico}'], - navigateFallbackDenylist: [/^\/api\//], - runtimeCaching: [ - { - urlPattern: /^https:\/\/.*\.monacousa\.org\/.*/i, - handler: 'NetworkFirst', - options: { - cacheName: 'api-cache', - expiration: { - maxEntries: 10, - maxAgeSeconds: 60 * 60 * 24 // 24 hours - }, - cacheableResponse: { - statuses: [0, 200] - } - } - } - ], - skipWaiting: true, - clientsClaim: true - }, - client: { - installPrompt: true, - periodicSyncForUpdates: 20 - }, - devOptions: { - enabled: true, - type: 'module' - } - }, - nitro: { - experimental: { - wasm: true - } - }, - runtimeConfig: { - // Server-side configuration - keycloak: { - issuer: process.env.NUXT_KEYCLOAK_ISSUER || "", - clientId: process.env.NUXT_KEYCLOAK_CLIENT_ID || "monacousa-portal", - clientSecret: process.env.NUXT_KEYCLOAK_CLIENT_SECRET || "", - callbackUrl: process.env.NUXT_KEYCLOAK_CALLBACK_URL || "https://monacousa.org/auth/callback", - }, - nocodb: { - url: process.env.NUXT_NOCODB_URL || "", - token: process.env.NUXT_NOCODB_TOKEN || "", - baseId: process.env.NUXT_NOCODB_BASE_ID || "", - }, - minio: { - endPoint: process.env.NUXT_MINIO_ENDPOINT || "s3.monacousa.org", - port: parseInt(process.env.NUXT_MINIO_PORT || "443"), - useSSL: process.env.NUXT_MINIO_USE_SSL !== "false", - accessKey: process.env.NUXT_MINIO_ACCESS_KEY || "", - secretKey: process.env.NUXT_MINIO_SECRET_KEY || "", - bucketName: process.env.NUXT_MINIO_BUCKET_NAME || "monacousa-portal", - }, - sessionSecret: process.env.NUXT_SESSION_SECRET || "", - encryptionKey: process.env.NUXT_ENCRYPTION_KEY || "", - public: { - // Client-side configuration - appName: "MonacoUSA Portal", - domain: process.env.NUXT_PUBLIC_DOMAIN || "monacousa.org", - }, - }, - vuetify: { - vuetifyOptions: { - theme: { - defaultTheme: "monacousa", - themes: { - monacousa: { - colors: { - primary: "#a31515", - secondary: "#ffffff", - accent: "#f5f5f5", - error: "#ff5252", - warning: "#ff9800", - info: "#2196f3", - success: "#4caf50", - }, - }, - }, - }, - }, - }, -}); -``` - -### 3.2 Create Environment Configuration (.env.example) - -```env -# Keycloak Configuration -NUXT_KEYCLOAK_ISSUER=https://auth.monacousa.org/realms/monacousa-portal -NUXT_KEYCLOAK_CLIENT_ID=monacousa-portal -NUXT_KEYCLOAK_CLIENT_SECRET=your-keycloak-client-secret -NUXT_KEYCLOAK_CALLBACK_URL=https://monacousa.org/auth/callback - -# NocoDB Configuration -NUXT_NOCODB_URL=https://db.monacousa.org -NUXT_NOCODB_TOKEN=your-nocodb-token -NUXT_NOCODB_BASE_ID=your-nocodb-base-id - -# MinIO Configuration -NUXT_MINIO_ENDPOINT=s3.monacousa.org -NUXT_MINIO_PORT=443 -NUXT_MINIO_USE_SSL=true -NUXT_MINIO_ACCESS_KEY=your-minio-access-key -NUXT_MINIO_SECRET_KEY=your-minio-secret-key -NUXT_MINIO_BUCKET_NAME=monacousa-portal - -# Security Configuration -NUXT_SESSION_SECRET=your-48-character-session-secret-key-here -NUXT_ENCRYPTION_KEY=your-32-character-encryption-key-here - -# Public Configuration -NUXT_PUBLIC_DOMAIN=monacousa.org -``` - -## Phase 4: Authentication Implementation - -### 4.1 Create Keycloak Utility (server/utils/keycloak.ts) - -```typescript -interface KeycloakConfig { - issuer: string; - clientId: string; - clientSecret: string; - callbackUrl: string; -} - -interface TokenResponse { - access_token: string; - refresh_token: string; - id_token: string; - token_type: string; - expires_in: number; -} - -interface UserInfo { - sub: string; - email: string; - given_name?: string; - family_name?: string; - name?: string; - groups?: string[]; - tier?: string; -} - -export class KeycloakClient { - private config: KeycloakConfig; - - constructor(config: KeycloakConfig) { - this.config = config; - } - - getAuthUrl(state: string): string { - const params = new URLSearchParams({ - client_id: this.config.clientId, - redirect_uri: this.config.callbackUrl, - response_type: 'code', - scope: 'openid email profile', - state, - }); - - return `${this.config.issuer}/protocol/openid-connect/auth?${params}`; - } - - async exchangeCodeForTokens(code: string): Promise { - const response = await fetch(`${this.config.issuer}/protocol/openid-connect/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - client_id: this.config.clientId, - client_secret: this.config.clientSecret, - code, - redirect_uri: this.config.callbackUrl, - }), - }); - - if (!response.ok) { - throw new Error(`Token exchange failed: ${response.statusText}`); - } - - return response.json(); - } - - async getUserInfo(accessToken: string): Promise { - const response = await fetch(`${this.config.issuer}/protocol/openid-connect/userinfo`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - - if (!response.ok) { - throw new Error(`Failed to get user info: ${response.statusText}`); - } - - return response.json(); - } - - async refreshToken(refreshToken: string): Promise { - const response = await fetch(`${this.config.issuer}/protocol/openid-connect/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - client_id: this.config.clientId, - client_secret: this.config.clientSecret, - refresh_token: refreshToken, - }), - }); - - if (!response.ok) { - throw new Error(`Token refresh failed: ${response.statusText}`); - } - - return response.json(); - } -} - -export function createKeycloakClient(): KeycloakClient { - const config = useRuntimeConfig(); - return new KeycloakClient(config.keycloak); -} -``` - -### 4.2 Create Session Management (server/utils/session.ts) - -```typescript -import { serialize, parse } from 'cookie'; -import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'crypto'; - -interface SessionData { - user: { - id: string; - email: string; - name: string; - groups?: string[]; - tier?: string; - }; - tokens: { - accessToken: string; - refreshToken: string; - expiresAt: number; - }; - createdAt: number; - lastActivity: number; -} - -export class SessionManager { - private encryptionKey: Buffer; - private cookieName = 'monacousa-session'; - - constructor(encryptionKey: string) { - this.encryptionKey = Buffer.from(encryptionKey, 'hex'); - } - - private encrypt(data: string): string { - const iv = randomBytes(16); - const cipher = createCipheriv('aes-256-cbc', this.encryptionKey, iv); - let encrypted = cipher.update(data, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - return iv.toString('hex') + ':' + encrypted; - } - - private decrypt(encryptedData: string): string { - const [ivHex, encrypted] = encryptedData.split(':'); - const iv = Buffer.from(ivHex, 'hex'); - const decipher = createDecipheriv('aes-256-cbc', this.encryptionKey, iv); - let decrypted = decipher.update(encrypted, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - return decrypted; - } - - createSession(sessionData: SessionData): string { - const data = JSON.stringify(sessionData); - const encrypted = this.encrypt(data); - - return serialize(this.cookieName, encrypted, { - httpOnly: true, - secure: true, - sameSite: 'lax', - maxAge: 60 * 60 * 24 * 7, // 7 days - path: '/', - }); - } - - getSession(cookieHeader?: string): SessionData | null { - if (!cookieHeader) return null; - - const cookies = parse(cookieHeader); - const sessionCookie = cookies[this.cookieName]; - - if (!sessionCookie) return null; - - try { - const decrypted = this.decrypt(sessionCookie); - const sessionData = JSON.parse(decrypted) as SessionData; - - // Check if session is expired - if (Date.now() > sessionData.tokens.expiresAt) { - return null; - } - - return sessionData; - } catch (error) { - console.error('Failed to decrypt session:', error); - return null; - } - } - - destroySession(): string { - return serialize(this.cookieName, '', { - httpOnly: true, - secure: true, - sameSite: 'lax', - maxAge: 0, - path: '/', - }); - } -} - -export function createSessionManager(): SessionManager { - const config = useRuntimeConfig(); - return new SessionManager(config.encryptionKey); -} -``` - -### 4.3 Create Authentication Middleware (middleware/auth.ts) - -```typescript -export default defineNuxtRouteMiddleware((to) => { - // Skip auth for public pages - if (to.meta.auth === false) { - return; - } - - // Check if user is authenticated - const authState = useState('auth.state', () => ({ - authenticated: false, - user: null, - groups: [], - })); - - if (!authState.value.authenticated) { - return navigateTo('/login'); - } -}); -``` - -### 4.4 Create Auth API Routes - -#### server/api/auth/login.get.ts - -```typescript -export default defineEventHandler(async (event) => { - const keycloak = createKeycloakClient(); - const state = randomBytes(32).toString('hex'); - - // Store state in session for verification - setCookie(event, 'oauth-state', state, { - httpOnly: true, - secure: true, - maxAge: 600, // 10 minutes - }); - - const authUrl = keycloak.getAuthUrl(state); - - return sendRedirect(event, authUrl); -}); -``` - -#### server/api/auth/callback.get.ts - -```typescript -export default defineEventHandler(async (event) => { - const query = getQuery(event); - const { code, state } = query; - - if (!code || !state) { - throw createError({ - statusCode: 400, - statusMessage: 'Missing authorization code or state', - }); - } - - // Verify state - const storedState = getCookie(event, 'oauth-state'); - if (state !== storedState) { - throw createError({ - statusCode: 400, - statusMessage: 'Invalid state parameter', - }); - } - - try { - const keycloak = createKeycloakClient(); - const sessionManager = createSessionManager(); - - // Exchange code for tokens - const tokens = await keycloak.exchangeCodeForTokens(code as string); - - // Get user info - const userInfo = await keycloak.getUserInfo(tokens.access_token); - - // Create session - const sessionData = { - user: { - id: userInfo.sub, - email: userInfo.email, - name: userInfo.name || `${userInfo.given_name} ${userInfo.family_name}`.trim(), - groups: userInfo.groups || [], - tier: userInfo.tier, - }, - tokens: { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token, - expiresAt: Date.now() + (tokens.expires_in * 1000), - }, - createdAt: Date.now(), - lastActivity: Date.now(), - }; - - const sessionCookie = sessionManager.createSession(sessionData); - - // Set session cookie - setHeader(event, 'Set-Cookie', sessionCookie); - - // Clear state cookie - deleteCookie(event, 'oauth-state'); - - return sendRedirect(event, '/dashboard'); - } catch (error) { - console.error('Auth callback error:', error); - throw createError({ - statusCode: 500, - statusMessage: 'Authentication failed', - }); - } -}); -``` - -#### server/api/auth/session.get.ts - -```typescript -export default defineEventHandler(async (event) => { - const sessionManager = createSessionManager(); - const cookieHeader = getHeader(event, 'cookie'); - const session = sessionManager.getSession(cookieHeader); - - if (!session) { - return { - authenticated: false, - user: null, - groups: [], - }; - } - - return { - authenticated: true, - user: session.user, - groups: session.user.groups || [], - }; -}); -``` - -#### server/api/auth/logout.post.ts - -```typescript -export default defineEventHandler(async (event) => { - const sessionManager = createSessionManager(); - const destroyCookie = sessionManager.destroySession(); - - setHeader(event, 'Set-Cookie', destroyCookie); - - return { success: true }; -}); -``` - -## Phase 5: Database Integration (NocoDB) - -### 5.1 Create NocoDB Utility (server/utils/nocodb.ts) - -```typescript -interface NocoDBConfig { - url: string; - token: string; - baseId: string; -} - -export class NocoDBClient { - private config: NocoDBConfig; - private baseUrl: string; - - constructor(config: NocoDBConfig) { - this.config = config; - this.baseUrl = `${config.url}/api/v2/tables`; - } - - private async request(endpoint: string, options: RequestInit = {}) { - const url = `${this.baseUrl}${endpoint}`; - - const response = await fetch(url, { - ...options, - headers: { - 'xc-token': this.config.token, - 'Content-Type': 'application/json', - ...options.headers, - }, - }); - - if (!response.ok) { - throw new Error(`NocoDB request failed: ${response.statusText}`); - } - - return response.json(); - } - - async findAll(tableName: string, params: Record = {}) { - const queryString = new URLSearchParams(params).toString(); - const endpoint = `/${tableName}/records${queryString ? `?${queryString}` : ''}`; - return this.request(endpoint); - } - - async findOne(tableName: string, id: string) { - return this.request(`/${tableName}/records/${id}`); - } - - async create(tableName: string, data: Record) { - return this.request(`/${tableName}/records`, { - method: 'POST', - body: JSON.stringify(data), - }); - } - - async update(tableName: string, id: string, data: Record) { - return this.request(`/${tableName}/records/${id}`, { - method: 'PATCH', - body: JSON.stringify(data), - }); - } - - async delete(tableName: string, id: string) { - return this.request(`/${tableName}/records/${id}`, { - method: 'DELETE', - }); - } -} - -export function createNocoDBClient(): NocoDBClient { - const config = useRuntimeConfig(); - return new NocoDBClient(config.nocodb); -} -``` - -### 5.2 Create Database API Template (server/api/data/[table]/[...params].ts) - -```typescript -export default defineEventHandler(async (event) => { - const method = getMethod(event); - const params = getRouterParams(event); - const table = params.table; - const additionalParams = params.params?.split('/') || []; - - const nocodb = createNocoDBClient(); - - try { - switch (method) { - case 'GET': - if (additionalParams.length > 0) { - // Get single record - const id = additionalParams[0]; - return await nocodb.findOne(table, id); - } else { - // Get all records - const query = getQuery(event); - return await nocodb.findAll(table, query); - } - - case 'POST': - const createData = await readBody(event); - return await nocodb.create(table, createData); - - case 'PATCH': - if (additionalParams.length === 0) { - throw createError({ - statusCode: 400, - statusMessage: 'Record ID required for update', - }); - } - const updateId = additionalParams[0]; - const updateData = await readBody(event); - return await nocodb.update(table, updateId, updateData); - - case 'DELETE': - if (additionalParams.length === 0) { - throw createError({ - statusCode: 400, - statusMessage: 'Record ID required for delete', - }); - } - const deleteId = additionalParams[0]; - return await nocodb.delete(table, deleteId); - - default: - throw createError({ - statusCode: 405, - statusMessage: 'Method not allowed', - }); - } - } catch (error) { - console.error('Database operation error:', error); - throw createError({ - statusCode: 500, - statusMessage: 'Database operation failed', - }); - } -}); -``` - -## Phase 6: File Storage Integration (MinIO) - -### 6.1 Create MinIO Utility (server/utils/minio.ts) - -```typescript -import { Client } from 'minio'; - -interface MinIOConfig { - endPoint: string; - port: number; - useSSL: boolean; - accessKey: string; - secretKey: string; - bucketName: string; -} - -export class MinIOClient { - private client: Client; - private bucketName: string; - - constructor(config: MinIOConfig) { - this.client = new Client({ - endPoint: config.endPoint, - port: config.port, - useSSL: config.useSSL, - accessKey: config.accessKey, - secretKey: config.secretKey, - }); - this.bucketName = config.bucketName; - } - - async ensureBucket(): Promise { - const exists = await this.client.bucketExists(this.bucketName); - if (!exists) { - await this.client.makeBucket(this.bucketName); - } - } - - async uploadFile(fileName: string, buffer: Buffer, contentType?: string): Promise { - await this.ensureBucket(); - - const metadata = contentType ? { 'Content-Type': contentType } : {}; - - await this.client.putObject(this.bucketName, fileName, buffer, buffer.length, metadata); - - return fileName; - } - - async getFile(fileName: string): Promise { - const stream = await this.client.getObject(this.bucketName, fileName); - const chunks: Buffer[] = []; - - return new Promise((resolve, reject) => { - stream.on('data', (chunk) => chunks.push(chunk)); - stream.on('end', () => resolve(Buffer.concat(chunks))); - stream.on('error', reject); - }); - } - - async deleteFile(fileName: string): Promise { - await this.client.removeObject(this.bucketName, fileName); - } - - async listFiles(prefix?: string): Promise { - const objects: string[] = []; - const stream = this.client.listObjects(this.bucketName, prefix); - - return new Promise((resolve, reject) => { - stream.on('data', (obj) => objects.push(obj.name!)); - stream.on('end', () => resolve(objects)); - stream.on('error', reject); - }); - } - - getPresignedUrl(fileName: string, expiry: number = 3600): Promise { - return this.client.presignedGetObject(this.bucketName, fileName, expiry); - } -} - -export function createMinIOClient(): MinIOClient { - const config = useRuntimeConfig(); - return new MinIOClient(config.minio); -} -``` - -### 6.2 Create File Upload API (server/api/files/upload.post.ts) - -```typescript -import formidable from 'formidable'; -import { readFileSync } from 'fs'; -import { lookup } from 'mime-types'; - -export default defineEventHandler(async (event) => { - const form = formidable({ - maxFileSize: 10 * 1024 * 1024, // 10MB - allowEmptyFiles: false, - }); - - try { - const [fields, files] = await form.parse(event.node.req); - const minio = createMinIOClient(); - - const uploadedFiles = []; - - for (const [fieldName, fileArray] of Object.entries(files)) { - const file = Array.isArray(fileArray) ? fileArray[0] : fileArray; - - if (file && file.filepath) { - const buffer = readFileSync(file.filepath); - const contentType = lookup(file.originalFilename || '') || 'application/octet-stream'; - const fileName = `${Date.now()}-${file.originalFilename}`; - - await minio.uploadFile(fileName, buffer, contentType); - - uploadedFiles.push({ - fieldName, - fileName, - originalName: file.originalFilename, - size: file.size, - contentType, - }); - } - } - - return { success: true, files: uploadedFiles }; - } catch (error) { - console.error('File upload error:', error); - throw createError({ - statusCode: 500, - statusMessage: 'File upload failed', - }); - } -}); -``` - -### 6.3 Create File Download API (server/api/files/[filename].get.ts) - -```typescript -export default defineEventHandler(async (event) => { - const filename = getRouterParam(event, 'filename'); - - if (!filename) { - throw createError({ - statusCode: 400, - statusMessage: 'Filename required', - }); - } - - try { - const minio = createMinIOClient(); - const buffer = await minio.getFile(filename); - - // Set appropriate headers - setHeader(event, 'Content-Type', 'application/octet-stream'); - setHeader(event, 'Content-Disposition', `attachment; filename="${filename}"`); - - return buffer; - } catch (error) { - console.error('File download error:', error); - throw createError({ - statusCode: 404, - statusMessage: 'File not found', - }); - } -}); -``` - -## Phase 7: UI Components and Layout - -### 7.1 Create Authentication Composable (composables/useAuth.ts) - -```typescript -interface User { - id: string; - email: string; - name: string; - groups?: string[]; - tier?: string; -} - -interface AuthState { - authenticated: boolean; - user: User | null; - groups: string[]; -} - -export const useAuth = () => { - const authState = useState('auth.state', () => ({ - authenticated: false, - user: null, - groups: [], - })); - - const login = () => { - return navigateTo('/api/auth/login'); - }; - - const logout = async () => { - try { - await $fetch('/api/auth/logout', { method: 'POST' }); - authState.value = { - authenticated: false, - user: null, - groups: [], - }; - await navigateTo('/login'); - } catch (error) { - console.error('Logout error:', error); - await navigateTo('/login'); - } - }; - - const checkAuth = async () => { - try { - const response = await $fetch('/api/auth/session'); - authState.value = response; - return response.authenticated; - } catch (error) { - console.error('Auth check error:', error); - authState.value = { - authenticated: false, - user: null, - groups: [], - }; - return false; - } - }; - - const isAdmin = computed(() => { - return authState.value.groups?.includes('admin') || false; - }); - - const hasRole = (role: string) => { - return authState.value.groups?.includes(role) || false; - }; - - return { - authState: readonly(authState), - user: computed(() => authState.value.user), - authenticated: computed(() => authState.value.authenticated), - groups: computed(() => authState.value.groups), - isAdmin, - hasRole, - login, - logout, - checkAuth, - }; -}; -``` - -### 7.2 Create Login Page (pages/login.vue) - -```vue - - - -``` - -### 7.3 Create Auth Callback Page (pages/auth/callback.vue) - -```vue - - - -``` - -### 7.4 Create Dashboard Layout (layouts/dashboard.vue) - -```vue - - - - - -``` - -### 7.5 Create Dashboard Index Page (pages/dashboard/index.vue) - -```vue - - - -``` - -### 7.6 Create Tools Page Template (pages/dashboard/tools.vue) - -```vue - - - -``` - -## Phase 8: Health Check and Startup - -### 8.1 Create Health Check API (server/api/health.get.ts) - -```typescript -export default defineEventHandler(async (event) => { - const checks = { - server: 'ok', - database: 'unknown', - storage: 'unknown', - auth: 'unknown', - }; - - try { - // Test NocoDB connection - const nocodb = createNocoDBClient(); - await nocodb.findAll('test', { limit: 1 }); - checks.database = 'ok'; - } catch (error) { - checks.database = 'error'; - } - - try { - // Test MinIO connection - const minio = createMinIOClient(); - await minio.ensureBucket(); - checks.storage = 'ok'; - } catch (error) { - checks.storage = 'error'; - } - - try { - // Test Keycloak connection - const keycloak = createKeycloakClient(); - // Simple connectivity test - you might want to implement a proper health check - checks.auth = 'ok'; - } catch (error) { - checks.auth = 'error'; - } - - const allHealthy = Object.values(checks).every(status => status === 'ok'); - - return { - status: allHealthy ? 'healthy' : 'degraded', - timestamp: new Date().toISOString(), - checks, - }; -}); -``` - -### 8.2 Create Startup Plugin (plugins/01.auth-check.client.ts) - -```typescript -export default defineNuxtPlugin(async () => { - const { checkAuth } = useAuth(); - - // Check authentication status on app startup - await checkAuth(); -}); -``` - -## Phase 9: TypeScript Types - -### 9.1 Create Type Definitions (utils/types.ts) - -```typescript -export interface User { - id: string; - email: string; - name: string; - groups?: string[]; - tier?: string; -} - -export interface AuthState { - authenticated: boolean; - user: User | null; - groups: string[]; -} - -export interface ApiResponse { - success: boolean; - data?: T; - error?: string; - message?: string; -} - -export interface FileUpload { - fieldName: string; - fileName: string; - originalName: string; - size: number; - contentType: string; -} - -export interface DatabaseRecord { - id: string; - created_at: string; - updated_at: string; - [key: string]: any; -} - -export interface HealthCheck { - status: 'healthy' | 'degraded' | 'unhealthy'; - timestamp: string; - checks: { - server: string; - database: string; - storage: string; - auth: string; - }; -} -``` - -## Phase 10: Final Setup and Testing - -### 10.1 Create Main App File (app.vue) - -```vue - - - -``` - -### 10.2 Create Index Page (pages/index.vue) - -```vue - - - -``` - -### 10.3 Create README.md - -```markdown -# MonacoUSA Portal - -A modern, responsive portal built with Nuxt 3, Vuetify, and Keycloak authentication. - -## Features - -- ๐Ÿ” **Keycloak Authentication** - Secure OAuth2/OIDC authentication -- ๐Ÿ“ฑ **Mobile Responsive** - Works perfectly on all devices -- ๐Ÿš€ **PWA Support** - Installable progressive web app -- ๐ŸŽจ **Modern UI** - Beautiful Vuetify 3 interface with MonacoUSA branding -- ๐Ÿ“ **File Storage** - MinIO S3-compatible file storage -- ๐Ÿ—„๏ธ **Database** - NocoDB for flexible data management -- ๐Ÿ”ง **Modular** - Easy to extend with new tools and features - -## Tech Stack - -- **Framework**: Nuxt 3 with Vue 3 -- **UI Library**: Vuetify 3 -- **Authentication**: Keycloak (OAuth2/OIDC) -- **Database**: NocoDB -- **File Storage**: MinIO (S3-compatible) -- **PWA**: Vite PWA plugin -- **TypeScript**: Full TypeScript support - -## Quick Start - -1. **Clone and Install** - ```bash - git clone - cd monacousa-portal - npm install - ``` - -2. **Environment Setup** - ```bash - cp .env.example .env - # Edit .env with your configuration - ``` - -3. **Development** - ```bash - npm run dev - ``` - -4. **Production Build** - ```bash - npm run build - npm run preview - ``` - -## Configuration - -### Environment Variables - -See `.env.example` for all required environment variables: - -- **Keycloak**: Authentication server configuration -- **NocoDB**: Database connection settings -- **MinIO**: File storage configuration -- **Security**: Encryption keys and session secrets - -### Keycloak Setup - -1. Create a new client in your Keycloak realm -2. Set client type to "Confidential" -3. Configure redirect URIs: - - `https://monacousa.org/auth/callback` - - `http://localhost:3000/auth/callback` (development) -4. Enable "Standard Flow" authentication -5. Set up user attributes and groups as needed - -### NocoDB Setup - -1. Set up your NocoDB instance -2. Create a new base/project -3. Generate an API token -4. Configure tables as needed for your tools - -### MinIO Setup - -1. Set up MinIO server -2. Create access keys -3. Configure bucket policies -4. Set CORS policies for web access - -## Project Structure - -``` -monacousa-portal/ -โ”œโ”€โ”€ components/ # Vue components -โ”œโ”€โ”€ composables/ # Vue composables -โ”œโ”€โ”€ layouts/ # Nuxt layouts -โ”œโ”€โ”€ middleware/ # Route middleware -โ”œโ”€โ”€ pages/ # Application pages -โ”‚ โ”œโ”€โ”€ auth/ # Authentication pages -โ”‚ โ””โ”€โ”€ dashboard/ # Dashboard pages -โ”œโ”€โ”€ plugins/ # Nuxt plugins -โ”œโ”€โ”€ public/ # Static assets -โ”œโ”€โ”€ server/ # Server-side code -โ”‚ โ”œโ”€โ”€ api/ # API routes -โ”‚ โ”œโ”€โ”€ utils/ # Server utilities -โ”‚ โ””โ”€โ”€ plugins/ # Server plugins -โ”œโ”€โ”€ utils/ # Shared utilities -โ””โ”€โ”€ docs/ # Documentation -``` - -## Development - -### Adding New Tools - -1. Create a new page in `pages/dashboard/` -2. Add navigation item to dashboard layout -3. Implement API routes in `server/api/` if needed -4. Add database tables in NocoDB if required - -### API Usage - -The portal provides RESTful APIs for data operations: - -```typescript -// Get all records from a table -const data = await $fetch('/api/data/users'); - -// Get single record -const user = await $fetch('/api/data/users/123'); - -// Create new record -const newUser = await $fetch('/api/data/users', { - method: 'POST', - body: { name: 'John Doe', email: 'john@example.com' } -}); - -// Update record -const updatedUser = await $fetch('/api/data/users/123', { - method: 'PATCH', - body: { name: 'Jane Doe' } -}); - -// Delete record -await $fetch('/api/data/users/123', { method: 'DELETE' }); -``` - -### File Upload - -```typescript -// Upload files -const formData = new FormData(); -formData.append('file', file); - -const result = await $fetch('/api/files/upload', { - method: 'POST', - body: formData -}); -``` - -## Deployment - -### Docker Deployment - -```dockerfile -FROM node:18-alpine - -WORKDIR /app -COPY package*.json ./ -RUN npm ci --only=production - -COPY . . -RUN npm run build - -EXPOSE 3000 -CMD ["npm", "run", "preview"] -``` - -### Environment Variables for Production - -Ensure all environment variables are properly set: -- Use strong encryption keys -- Configure proper domain names -- Set up SSL certificates -- Configure firewall rules - -## Health Checks - -The portal includes health check endpoints: - -- `GET /api/health` - Overall system health -- Check database connectivity -- Check file storage connectivity -- Check authentication service - -## Security - -- All sessions are encrypted -- HTTPS required in production -- CSRF protection enabled -- Secure cookie settings -- Input validation on all APIs - -## Support - -For issues and questions: -1. Check the documentation -2. Review environment configuration -3. Check health endpoints -4. Review server logs - -## License - -[Your License Here] -``` - -### 10.4 Create TypeScript Configuration (tsconfig.json) - -```json -{ - "extends": "./.nuxt/tsconfig.json" -} -``` - -### 10.5 Create Git Ignore (.gitignore) - -```gitignore -# Nuxt dev/build outputs -.output -.nuxt -.nitro -.cache -dist - -# Node dependencies -node_modules - -# Logs -*.log* - -# Misc -.DS_Store -.fleet -.idea - -# Local env files -.env -.env.* -!.env.example - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? -``` - -## Phase 11: Testing and Verification - -### 11.1 Development Testing - -After setting up the project, test the following: - -1. **Authentication Flow** - ```bash - npm run dev - # Visit http://localhost:3000 - # Test login/logout flow - ``` - -2. **API Endpoints** - ```bash - # Test health check - curl http://localhost:3000/api/health - - # Test authentication session - curl http://localhost:3000/api/auth/session - ``` - -3. **File Upload** - - Test file upload functionality - - Verify MinIO storage - - Check file download - -4. **Database Operations** - - Test CRUD operations - - Verify NocoDB integration - - Check data persistence - -### 11.2 Production Checklist - -Before deploying to production: - -- [ ] All environment variables configured -- [ ] SSL certificates installed -- [ ] Keycloak client properly configured -- [ ] NocoDB accessible and secured -- [ ] MinIO bucket policies configured -- [ ] Health checks passing -- [ ] PWA manifest and icons in place -- [ ] Error handling tested -- [ ] Mobile responsiveness verified - -## Phase 12: Deployment Guide - -### 12.1 Server Requirements - -- Node.js 18+ -- SSL certificate -- Reverse proxy (nginx/Apache) -- Firewall configuration - -### 12.2 Environment Setup - -1. **Production Environment Variables** - ```bash - # Copy and configure - cp .env.example .env.production - ``` - -2. **Build Application** - ```bash - npm run build - ``` - -3. **Start Production Server** - ```bash - npm run preview - # or use PM2 for process management - pm2 start ecosystem.config.js - ``` - -### 12.3 Nginx Configuration Example - -```nginx -server { - listen 443 ssl http2; - server_name monacousa.org; - - ssl_certificate /path/to/certificate.crt; - ssl_certificate_key /path/to/private.key; - - location / { - proxy_pass http://localhost:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - } -} -``` - -## Conclusion - -This implementation guide provides a complete foundation for the MonacoUSA Portal. The portal includes: - -โœ… **Complete Authentication System** with Keycloak integration -โœ… **Responsive Dashboard** that works on all devices -โœ… **File Storage System** with MinIO integration -โœ… **Database Integration** with NocoDB -โœ… **PWA Support** for mobile installation -โœ… **Modern UI** with Vuetify and MonacoUSA branding -โœ… **Modular Architecture** for easy extension -โœ… **Production-Ready** configuration and deployment guide - -The foundation is ready for you to build your custom tools and features on top of this solid, proven architecture. diff --git a/KEYCLOAK_CUSTOM_LOGIN_IMPLEMENTATION_GUIDE.md b/KEYCLOAK_CUSTOM_LOGIN_IMPLEMENTATION_GUIDE.md deleted file mode 100644 index 00c1feb..0000000 --- a/KEYCLOAK_CUSTOM_LOGIN_IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -1,1482 +0,0 @@ -# ๐Ÿ” Keycloak Custom Login Implementation Guide - -## ๐Ÿ“‹ **Table of Contents** - -1. [Overview & Architecture](#overview--architecture) -2. [Keycloak Configuration](#keycloak-configuration) -3. [Environment Setup](#environment-setup) -4. [Server-Side Implementation](#server-side-implementation) -5. [Client-Side Implementation](#client-side-implementation) -6. [Authentication Flow](#authentication-flow) -7. [Session Management](#session-management) -8. [Security Implementation](#security-implementation) -9. [Middleware System](#middleware-system) -10. [Error Handling](#error-handling) -11. [Testing & Debugging](#testing--debugging) -12. [Production Considerations](#production-considerations) -13. [Troubleshooting](#troubleshooting) - -## ๐Ÿ—๏ธ **Overview & Architecture** - -### **Why Custom Login Instead of Keycloak Hosted Pages** - -**Benefits:** -- โœ… **Complete UI Control**: Custom branding, responsive design, mobile optimization -- โœ… **Better UX**: No redirects to external domains, seamless user experience -- โœ… **Integration**: Easy integration with existing Nuxt/Vue components -- โœ… **Mobile Compatibility**: Full control over mobile browser behavior -- โœ… **Performance**: No external redirects, faster login experience - -**Trade-offs:** -- โš ๏ธ **More Complex**: Must handle security, session management, token validation -- โš ๏ธ **Security Responsibility**: Must implement proper rate limiting, CSRF protection -- โš ๏ธ **Maintenance**: More code to maintain vs. Keycloak's hosted pages - -### **Architecture Overview** - -```mermaid -graph TD - A[Client Browser] --> B[Nuxt App] - B --> C[Custom Login Page] - C --> D[Server API Route] - D --> E[Keycloak Token Endpoint] - E --> F[Return Access Token] - F --> D - D --> G[Create Encrypted Session] - G --> H[Set HTTP-Only Cookie] - H --> B - B --> I[Authenticated App] -``` - -### **Tech Stack** - -- **Frontend**: Nuxt 3 + Vue 3 + Vuetify 3 -- **Backend**: Nuxt 3 Server Routes -- **Authentication**: Keycloak (Resource Owner Password Credentials flow) -- **Session**: Encrypted server-side session storage -- **Security**: Rate limiting, input validation, CSRF protection - -## ๐Ÿ”ง **Keycloak Configuration** - -### **1. Client Configuration** - -In Keycloak Admin Console: - -```javascript -// Client Settings -{ - clientId: "monacousa-portal", - clientType: "confidential", // Important: Must be confidential - standardFlowEnabled: false, // Disable standard flow - directAccessGrantsEnabled: true, // Enable direct access grants - serviceAccountsEnabled: false, - publicClient: false, - - // Valid Redirect URIs (for OAuth fallback if needed) - redirectUris: [ - "https://yourdomain.com/auth/callback", - "http://localhost:3000/auth/callback" // Dev only - ], - - // Root URL - rootUrl: "https://yourdomain.com", - adminUrl: "https://yourdomain.com", - baseUrl: "https://yourdomain.com", - - // Advanced Settings - accessTokenLifespan: "15 minutes", - ssoSessionIdleTimeout: "30 minutes", - ssoSessionMaxLifespan: "12 hours" -} -``` - -### **2. Required Scopes** - -```javascript -// Default Client Scopes (include these) -[ - "openid", // OpenID Connect - "profile", // User profile information - "email", // Email address - "roles", // User roles/groups - "groups" // User groups (if using) -] -``` - -### **3. User Groups/Roles Setup** - -```javascript -// Example Group Structure -{ - groups: [ - { - name: "admin", - description: "System administrators" - }, - { - name: "board", - description: "Board members" - }, - { - name: "user", - description: "Regular users" - } - ] -} -``` - -### **4. Keycloak Client Secret** - -```bash -# In Keycloak Admin Console: -# Clients โ†’ [Your Client] โ†’ Credentials โ†’ Client Secret -# Copy this secret for environment variables -``` - -## ๐ŸŒ **Environment Setup** - -### **Environment Variables** - -```bash -# .env -# Keycloak Configuration -NUXT_KEYCLOAK_ISSUER=https://auth.yourdomain.com/realms/your-realm -NUXT_KEYCLOAK_CLIENT_ID=your-client-id -NUXT_KEYCLOAK_CLIENT_SECRET=your-client-secret -NUXT_KEYCLOAK_CALLBACK_URL=https://yourdomain.com/auth/callback - -# Session Security -NUXT_SESSION_SECRET=your-48-character-session-secret-key-here-123456 -NUXT_ENCRYPTION_KEY=your-32-character-encryption-key-here12 - -# Public Configuration -NUXT_PUBLIC_DOMAIN=yourdomain.com -``` - -### **Nuxt Configuration** - -```typescript -// nuxt.config.ts -export default defineNuxtConfig({ - runtimeConfig: { - // Private (server-side only) - keycloak: { - issuer: process.env.NUXT_KEYCLOAK_ISSUER, - clientId: process.env.NUXT_KEYCLOAK_CLIENT_ID, - clientSecret: process.env.NUXT_KEYCLOAK_CLIENT_SECRET, - callbackUrl: process.env.NUXT_KEYCLOAK_CALLBACK_URL - }, - sessionSecret: process.env.NUXT_SESSION_SECRET, - encryptionKey: process.env.NUXT_ENCRYPTION_KEY, - - // Public (client-side accessible) - public: { - domain: process.env.NUXT_PUBLIC_DOMAIN - } - }, - - // SSR Configuration for auth - ssr: false, // or configure properly for SSR - - // Security headers - nitro: { - experimental: { - wasm: true - } - } -}) -``` - -## ๐Ÿ–ฅ๏ธ **Server-Side Implementation** - -### **1. Direct Login API Route** - -```typescript -// server/api/auth/direct-login.post.ts -export default defineEventHandler(async (event) => { - try { - const { username, password, rememberMe } = await readBody(event); - const config = useRuntimeConfig(); - - // Input validation - if (!username || !password) { - throw createError({ - statusCode: 400, - statusMessage: 'Username and password are required' - }); - } - - // Rate limiting - const clientIP = getClientIP(event); - if (!checkRateLimit(clientIP)) { - throw createError({ - statusCode: 429, - statusMessage: 'Too many login attempts' - }); - } - - // Direct authentication with Keycloak - const tokenResponse = await fetch(`${config.keycloak.issuer}/protocol/openid-connect/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': 'YourApp/1.0' - }, - body: new URLSearchParams({ - grant_type: 'password', - client_id: config.keycloak.clientId, - client_secret: config.keycloak.clientSecret, - username, - password, - scope: 'openid email profile roles' - }) - }); - - if (!tokenResponse.ok) { - throw createError({ - statusCode: 401, - statusMessage: 'Invalid credentials' - }); - } - - const tokens = await tokenResponse.json(); - - // Get user info from Keycloak - const userResponse = await fetch(`${config.keycloak.issuer}/protocol/openid-connect/userinfo`, { - headers: { - 'Authorization': `Bearer ${tokens.access_token}` - } - }); - - const userInfo = await userResponse.json(); - - // Extract user groups and determine tier - const groups = extractUserGroups(tokens, userInfo); - const userTier = determineTier(groups); - - // Create session - const sessionData = { - user: { - id: userInfo.sub, - email: userInfo.email, - name: userInfo.name, - firstName: userInfo.given_name, - lastName: userInfo.family_name, - tier: userTier, - groups: groups - }, - tokens: { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token, - expiresAt: Date.now() + (tokens.expires_in * 1000) - }, - rememberMe: !!rememberMe, - createdAt: Date.now(), - lastActivity: Date.now() - }; - - // Create encrypted session cookie - const sessionManager = createSessionManager(); - const cookieString = sessionManager.createSession(sessionData, !!rememberMe); - - // Set secure cookie - const maxAge = !!rememberMe ? 60 * 60 * 24 * 30 : 60 * 60 * 24 * 7; - const isProduction = process.env.NODE_ENV === 'production'; - - setCookie(event, 'your-session-name', cookieString.split('=')[1], { - httpOnly: true, - secure: isProduction, - sameSite: 'lax', - maxAge, - path: '/' - }); - - return { - success: true, - redirectTo: '/dashboard', - user: { - email: userInfo.email, - name: userInfo.name, - tier: userTier - } - }; - - } catch (error) { - // Error handling - throw createError({ - statusCode: error.statusCode || 500, - statusMessage: error.statusMessage || 'Authentication failed' - }); - } -}); -``` - -### **2. Session Validation API** - -```typescript -// server/api/auth/session.get.ts -export default defineEventHandler(async (event) => { - try { - const sessionManager = createSessionManager(); - const sessionData = sessionManager.getSession(event); - - if (!sessionData || !sessionData.user) { - return { - authenticated: false, - user: null - }; - } - - // Check if token needs refresh - if (sessionData.tokens.expiresAt < Date.now() + 60000) { // 1 minute buffer - try { - const refreshedTokens = await refreshKeycloakToken(sessionData.tokens.refreshToken); - - // Update session with new tokens - sessionData.tokens = { - accessToken: refreshedTokens.access_token, - refreshToken: refreshedTokens.refresh_token, - expiresAt: Date.now() + (refreshedTokens.expires_in * 1000) - }; - - sessionManager.updateSession(event, sessionData); - } catch (refreshError) { - console.warn('Token refresh failed:', refreshError); - // Session is invalid, force logout - return { - authenticated: false, - user: null - }; - } - } - - // Update last activity - sessionData.lastActivity = Date.now(); - sessionManager.updateSession(event, sessionData); - - return { - authenticated: true, - user: sessionData.user - }; - - } catch (error) { - console.error('Session validation error:', error); - return { - authenticated: false, - user: null - }; - } -}); -``` - -### **3. Session Management Utilities** - -```typescript -// server/utils/session.ts -import crypto from 'crypto'; - -interface SessionData { - user: User; - tokens: { - accessToken: string; - refreshToken: string; - expiresAt: number; - }; - rememberMe: boolean; - createdAt: number; - lastActivity: number; -} - -export const createSessionManager = () => { - const config = useRuntimeConfig(); - - const encryptData = (data: any): string => { - const cipher = crypto.createCipher('aes-256-cbc', config.encryptionKey); - let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex'); - encrypted += cipher.final('hex'); - return encrypted; - }; - - const decryptData = (encryptedData: string): any => { - try { - const decipher = crypto.createDecipher('aes-256-cbc', config.encryptionKey); - let decrypted = decipher.update(encryptedData, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - return JSON.parse(decrypted); - } catch (error) { - return null; - } - }; - - const createSession = (sessionData: SessionData, rememberMe: boolean): string => { - const sessionId = crypto.randomUUID(); - const encryptedData = encryptData(sessionData); - - // Store in server-side storage (memory, database, Redis, etc.) - serverSessionStorage.set(sessionId, encryptedData); - - return `session-id=${sessionId}`; - }; - - const getSession = (event: any): SessionData | null => { - const sessionId = getCookie(event, 'your-session-name'); - if (!sessionId) return null; - - const encryptedData = serverSessionStorage.get(sessionId); - if (!encryptedData) return null; - - return decryptData(encryptedData); - }; - - return { - createSession, - getSession, - updateSession: (event: any, sessionData: SessionData) => { - const sessionId = getCookie(event, 'your-session-name'); - if (sessionId) { - const encryptedData = encryptData(sessionData); - serverSessionStorage.set(sessionId, encryptedData); - } - }, - destroySession: (event: any) => { - const sessionId = getCookie(event, 'your-session-name'); - if (sessionId) { - serverSessionStorage.delete(sessionId); - deleteCookie(event, 'your-session-name'); - } - } - }; -}; -``` - -## ๐ŸŽจ **Client-Side Implementation** - -### **1. Authentication Composable** - -```typescript -// composables/useAuth.ts -export const useAuth = () => { - // Use useState for SSR compatibility - const user = useState('auth.user', () => null); - const isAuthenticated = computed(() => !!user.value); - const loading = ref(false); - const error = ref(null); - - // Tier-based computed properties - const userTier = computed(() => user.value?.tier || 'user'); - const isUser = computed(() => user.value?.tier === 'user'); - const isBoard = computed(() => user.value?.tier === 'board'); - const isAdmin = computed(() => user.value?.tier === 'admin'); - - // Login method - const login = async (credentials: LoginCredentials) => { - loading.value = true; - error.value = null; - - try { - const response = await $fetch('/api/auth/direct-login', { - method: 'POST', - body: credentials, - timeout: 30000 - }); - - if (response.success) { - // Wait for cookie to be set - await new Promise(resolve => setTimeout(resolve, 200)); - - // Verify session was created properly - let sessionSuccess = false; - let attempts = 0; - const maxAttempts = 3; - - while (!sessionSuccess && attempts < maxAttempts) { - attempts++; - sessionSuccess = await checkAuth(); - - if (!sessionSuccess && attempts < maxAttempts) { - await new Promise(resolve => setTimeout(resolve, 500)); - } - } - - if (sessionSuccess) { - return { - success: true, - redirectTo: response.redirectTo || '/dashboard' - }; - } - } - - return { success: false, error: 'Login failed' }; - } catch (err: any) { - const errorMessage = getErrorMessage(err); - error.value = errorMessage; - return { success: false, error: errorMessage }; - } finally { - loading.value = false; - } - }; - - // Check authentication status - const checkAuth = async (): Promise => { - try { - const response = await $fetch('/api/auth/session'); - - if (response.authenticated && response.user) { - user.value = response.user; - return true; - } else { - user.value = null; - return false; - } - } catch (err) { - user.value = null; - return false; - } - }; - - // Logout method - const logout = async () => { - try { - await $fetch('/api/auth/logout', { method: 'POST' }); - user.value = null; - await navigateTo('/login'); - } catch (err) { - user.value = null; - await navigateTo('/login'); - } - }; - - return { - user: readonly(user), - isAuthenticated, - loading: readonly(loading), - error: readonly(error), - userTier, - isUser, - isBoard, - isAdmin, - login, - logout, - checkAuth - }; -}; -``` - -### **2. Custom Login Page** - -```vue - - - - -``` - -### **3. Auth Plugin (Initialize Auth State)** - -```typescript -// plugins/01.auth-check.client.ts -export default defineNuxtPlugin(async () => { - const { checkAuth } = useAuth(); - - // Check authentication status on app startup - await checkAuth(); -}); -``` - -## ๐Ÿ›ก๏ธ **Security Implementation** - -### **1. Rate Limiting** - -```typescript -// server/utils/security.ts -const loginAttempts = new Map(); -const blockedIPs = new Map(); - -export const checkRateLimit = (ip: string): { allowed: boolean; attemptsLeft: number } => { - const now = Date.now(); - const maxAttempts = 5; - const windowMs = 15 * 60 * 1000; // 15 minutes - const blockDurationMs = 60 * 60 * 1000; // 1 hour - - // Check if IP is blocked - const blockedUntil = blockedIPs.get(ip); - if (blockedUntil && now < blockedUntil) { - return { allowed: false, attemptsLeft: 0 }; - } - - // Clear expired blocks - if (blockedUntil && now >= blockedUntil) { - blockedIPs.delete(ip); - } - - // Check current attempts - const attempts = loginAttempts.get(ip); - if (attempts && (now - attempts.lastAttempt) > windowMs) { - loginAttempts.delete(ip); - return { allowed: true, attemptsLeft: maxAttempts }; - } - - const currentCount = attempts?.count || 0; - if (currentCount >= maxAttempts) { - blockedIPs.set(ip, now + blockDurationMs); - return { allowed: false, attemptsLeft: 0 }; - } - - return { allowed: true, attemptsLeft: maxAttempts - currentCount }; -}; - -export const recordFailedAttempt = (ip: string): void => { - const now = Date.now(); - const attempts = loginAttempts.get(ip); - - if (attempts) { - attempts.count += 1; - attempts.lastAttempt = now; - } else { - loginAttempts.set(ip, { count: 1, lastAttempt: now }); - } -}; -``` - -### **2. Input Validation** - -```typescript -// server/utils/validation.ts -export const validateLoginInput = (username: string, password: string): string[] => { - const errors: string[] = []; - - // Username validation - if (!username || typeof username !== 'string') { - errors.push('Username is required'); - } else if (username.length < 2) { - errors.push('Username must be at least 2 characters'); - } else if (username.length > 100) { - errors.push('Username is too long'); - } else if (!/^[a-zA-Z0-9@._-]+$/.test(username)) { - errors.push('Username contains invalid characters'); - } - - // Password validation - if (!password || typeof password !== 'string') { - errors.push('Password is required'); - } else if (password.length < 6) { - errors.push('Password must be at least 6 characters'); - } else if (password.length > 200) { - errors.push('Password is too long'); - } - - return errors; -}; - -export const getClientIP = (event: any): string => { - const headers = getHeaders(event); - return ( - headers['x-forwarded-for']?.split(',')[0]?.trim() || - headers['x-real-ip'] || - headers['x-client-ip'] || - headers['cf-connecting-ip'] || - 'unknown' - ); -}; -``` - -### **3. Session Security** - -```typescript -// Secure cookie configuration -setCookie(event, 'session-name', sessionId, { - httpOnly: true, // Prevent XSS attacks - secure: isProduction, // HTTPS only in production - sameSite: 'lax', // CSRF protection - maxAge: rememberMe ? 2592000 : 604800, // 30 days vs 7 days - path: '/', // Available site-wide - domain: process.env.NUXT_PUBLIC_DOMAIN ? `.${process.env.NUXT_PUBLIC_DOMAIN}` : undefined -}); -``` - -## ๐Ÿ”„ **Authentication Flow** - -### **Complete Authentication Flow Diagram** - -```mermaid -sequenceDiagram - participant U as User - participant C as Client App - participant S as Server API - participant K as Keycloak - participant D as Database - - U->>C: Navigate to /login - C->>C: Guest middleware (allow if not authenticated) - C->>U: Show login form - - U->>C: Submit credentials - C->>S: POST /api/auth/direct-login - S->>S: Validate input & rate limiting - S->>K: POST /token (ROPC flow) - K->>S: Return access token + refresh token - S->>K: GET /userinfo (with access token) - K->>S: Return user profile - S->>S: Extract groups/roles, determine tier - S->>D: Create encrypted session - S->>C: Set secure cookie + return success - C->>C: Navigate to /dashboard - C->>S: GET /api/auth/session (via cookie) - S->>S: Decrypt session, validate tokens - S->>C: Return user data - C->>C: Auth middleware passes - C->>U: Show authenticated dashboard -``` - -### **Session Lifecycle** - -1. **Login**: User provides credentials โ†’ Server validates with Keycloak โ†’ Creates encrypted session -2. **Session Check**: Client calls `/api/auth/session` โ†’ Server validates session โ†’ Returns user data -3. **Token Refresh**: Server automatically refreshes tokens before expiry -4. **Logout**: Client calls `/api/auth/logout` โ†’ Server destroys session โ†’ Redirects to login - -## ๐Ÿ› ๏ธ **Middleware System** - -### **1. Auth Middleware (Protect Routes)** - -```typescript -// middleware/auth.ts -export default defineNuxtRouteMiddleware(async (to) => { - if (to.meta.auth === false) { - return; // Skip auth for public pages - } - - const { isAuthenticated, checkAuth, user } = useAuth(); - - // Ensure auth is checked if user isn't loaded - if (!user.value) { - await checkAuth(); - } - - if (!isAuthenticated.value) { - return navigateTo('/login'); - } -}); -``` - -### **2. Guest Middleware (Redirect Authenticated Users)** - -```typescript -// middleware/guest.ts -export default defineNuxtRouteMiddleware((to, from) => { - const { user } = useAuth(); - - // If user is already authenticated, redirect to dashboard - if (user.value) { - return navigateTo('/dashboard'); - } -}); -``` - -### **3. Role-Based Middleware** - -```typescript -// middleware/auth-admin.ts -export default defineNuxtRouteMiddleware((to, from) => { - const { isAuthenticated, isAdmin } = useAuth(); - - if (!isAuthenticated.value) { - return navigateTo('/login'); - } - - if (!isAdmin.value) { - throw createError({ - statusCode: 403, - statusMessage: 'Access denied. Administrator privileges required.' - }); - } -}); -``` - -## ๐Ÿงช **Testing & Debugging** - -### **1. Testing Checklist** - -**Backend Testing:** -```bash -# Test direct login endpoint -curl -X POST http://localhost:3000/api/auth/direct-login \ - -H "Content-Type: application/json" \ - -d '{"username":"test@example.com","password":"password123"}' - -# Test session endpoint with cookie -curl -X GET http://localhost:3000/api/auth/session \ - -H "Cookie: your-session-name=session-id-here" - -# Test logout endpoint -curl -X POST http://localhost:3000/api/auth/logout \ - -H "Cookie: your-session-name=session-id-here" -``` - -**Frontend Testing:** -- [ ] **Login Form Validation**: Test empty fields, short passwords, invalid characters -- [ ] **Login Flow**: Valid credentials โ†’ successful login โ†’ redirect to dashboard -- [ ] **Error Handling**: Invalid credentials โ†’ proper error messages -- [ ] **Session Management**: Page refresh โ†’ user stays logged in -- [ ] **Mobile Compatibility**: Test on iOS Safari, Android Chrome -- [ ] **Remember Me**: Long-term sessions work correctly - -**Security Testing:** -- [ ] **Rate Limiting**: 5+ failed attempts โ†’ IP blocked for 1 hour -- [ ] **CSRF Protection**: Direct API calls without cookies โ†’ rejected -- [ ] **XSS Protection**: Malicious input โ†’ properly sanitized -- [ ] **Session Security**: HttpOnly cookies โ†’ not accessible via JavaScript - -### **2. Debug Tools** - -**Server-Side Debugging:** -```typescript -// Add debug logging in server routes -console.log('๐Ÿ”„ Login attempt:', { - username: credentials.username, - ip: getClientIP(event), - timestamp: new Date().toISOString() -}); - -console.log('โœ… Keycloak response:', { - status: tokenResponse.status, - hasTokens: !!tokens.access_token, - userEmail: userInfo.email, - groups: extractedGroups -}); -``` - -**Client-Side Debugging:** -```typescript -// Add debug logging in composables -const checkAuth = async (): Promise => { - console.log('๐Ÿ”„ Checking authentication...'); - - try { - const response = await $fetch('/api/auth/session'); - - console.log('๐Ÿ” Session response:', { - authenticated: response.authenticated, - user: response.user?.email, - tier: response.user?.tier - }); - - // ... rest of method - } catch (err) { - console.error('โŒ Auth check failed:', err); - // ... error handling - } -}; -``` - -**Browser DevTools Inspection:** -- **Network Tab**: Monitor API calls to `/api/auth/*` -- **Application Tab**: Check cookies are set correctly -- **Console Tab**: Review authentication logs -- **Sources Tab**: Set breakpoints in auth flow - -## ๐Ÿšจ **Error Handling** - -### **1. Server-Side Error Handling** - -```typescript -// server/api/auth/direct-login.post.ts -export default defineEventHandler(async (event) => { - try { - // ... authentication logic - - } catch (error: any) { - console.error('โŒ Login error:', { - message: error.message, - status: error.status, - ip: getClientIP(event), - timestamp: new Date().toISOString() - }); - - // Map specific errors to user-friendly messages - if (error.status === 401) { - recordFailedAttempt(getClientIP(event)); - - throw createError({ - statusCode: 401, - statusMessage: 'Invalid username or password' - }); - } - - if (error.status === 429) { - throw createError({ - statusCode: 429, - statusMessage: 'Too many login attempts. Please try again later.' - }); - } - - if (error.code === 'ECONNREFUSED') { - throw createError({ - statusCode: 503, - statusMessage: 'Authentication service temporarily unavailable' - }); - } - - // Generic error for unexpected issues - throw createError({ - statusCode: 500, - statusMessage: 'Login failed. Please try again.' - }); - } -}); -``` - -### **2. Client-Side Error Handling** - -```typescript -// composables/useAuth.ts -const getErrorMessage = (err: any): string => { - // Handle network errors - if (!err.response) { - return 'Network error. Please check your connection.'; - } - - // Handle HTTP errors - switch (err.status) { - case 400: - return err.data?.message || 'Invalid request. Please check your input.'; - case 401: - return 'Invalid username or password.'; - case 403: - return 'Access denied. Please contact your administrator.'; - case 429: - return 'Too many login attempts. Please try again later.'; - case 502: - case 503: - return 'Service temporarily unavailable. Please try again.'; - case 504: - return 'Request timeout. Please try again.'; - default: - return err.data?.message || 'Login failed. Please try again.'; - } -}; -``` - -### **3. User-Friendly Error Display** - -```vue - - - -
- Login Failed -
- {{ loginError }} -
- -
-``` - -## ๐Ÿš€ **Production Considerations** - -### **1. Environment Configuration** - -```bash -# Production .env -NODE_ENV=production - -# Use strong secrets in production -NUXT_SESSION_SECRET=generate-a-very-strong-48-character-secret-key-here -NUXT_ENCRYPTION_KEY=generate-strong-32-char-key-here - -# Keycloak production URLs -NUXT_KEYCLOAK_ISSUER=https://auth.yourdomain.com/realms/your-realm -NUXT_KEYCLOAK_CLIENT_ID=your-production-client -NUXT_KEYCLOAK_CLIENT_SECRET=your-production-secret - -# Security settings -NUXT_PUBLIC_DOMAIN=yourdomain.com -``` - -### **2. Security Headers** - -```typescript -// nuxt.config.ts -export default defineNuxtConfig({ - nitro: { - routeRules: { - '/**': { - headers: { - // Security headers - 'X-Frame-Options': 'DENY', - 'X-Content-Type-Options': 'nosniff', - 'X-XSS-Protection': '1; mode=block', - 'Referrer-Policy': 'strict-origin-when-cross-origin', - 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()', - - // HTTPS enforcement - 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', - - // Content Security Policy - 'Content-Security-Policy': [ - "default-src 'self'", - "script-src 'self' 'unsafe-inline'", - "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", - "font-src 'self' https://fonts.gstatic.com", - "img-src 'self' data: https:", - "connect-src 'self'" - ].join('; ') - } - } - } - } -}); -``` - -### **3. Session Storage Options** - -**In-Memory (Development Only):** -```typescript -// Simple Map - loses data on restart -const sessions = new Map(); -``` - -**Redis (Recommended for Production):** -```typescript -// server/utils/session-storage.ts -import Redis from 'ioredis'; - -const redis = new Redis({ - host: process.env.REDIS_HOST || 'localhost', - port: parseInt(process.env.REDIS_PORT || '6379'), - password: process.env.REDIS_PASSWORD, - db: 0, - retryDelayOnFailover: 100, - maxRetriesPerRequest: 3 -}); - -export const sessionStorage = { - async set(key: string, value: string, ttl: number = 3600): Promise { - await redis.setex(`session:${key}`, ttl, value); - }, - - async get(key: string): Promise { - return await redis.get(`session:${key}`); - }, - - async delete(key: string): Promise { - await redis.del(`session:${key}`); - } -}; -``` - -**Database (Alternative):** -```sql --- Session table schema -CREATE TABLE user_sessions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - session_id VARCHAR(255) UNIQUE NOT NULL, - user_id VARCHAR(255) NOT NULL, - session_data TEXT NOT NULL, - expires_at TIMESTAMP NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_sessions_session_id ON user_sessions(session_id); -CREATE INDEX idx_sessions_expires_at ON user_sessions(expires_at); -``` - -### **4. Monitoring & Logging** - -```typescript -// server/utils/monitoring.ts -export const logAuthEvent = (event: string, data: any) => { - const logData = { - timestamp: new Date().toISOString(), - event, - ip: data.ip || 'unknown', - userAgent: data.userAgent || 'unknown', - user: data.user || 'anonymous', - success: data.success || false, - error: data.error || null - }; - - // Send to monitoring service (e.g., DataDog, New Relic, etc.) - console.log(JSON.stringify(logData)); - - // In production, send to your logging service: - // await sendToLoggingService(logData); -}; - -// Usage in auth routes -logAuthEvent('login_attempt', { - ip: getClientIP(event), - user: username, - success: true -}); -``` - -### **5. Performance Optimization** - -```typescript -// Cache Keycloak public key for token validation -let keycloakPublicKey: string | null = null; -let keyLastFetched = 0; -const KEY_CACHE_DURATION = 3600000; // 1 hour - -const getKeycloakPublicKey = async (): Promise => { - const now = Date.now(); - - if (!keycloakPublicKey || (now - keyLastFetched) > KEY_CACHE_DURATION) { - const config = useRuntimeConfig(); - const response = await fetch(`${config.keycloak.issuer}/.well-known/openid_configuration`); - const oidcConfig = await response.json(); - - const keysResponse = await fetch(oidcConfig.jwks_uri); - const keys = await keysResponse.json(); - - // Extract public key from JWKS - keycloakPublicKey = keys.keys[0].x5c[0]; - keyLastFetched = now; - } - - return keycloakPublicKey!; -}; -``` - -## ๐Ÿ”ง **Troubleshooting** - -### **1. Common Issues** - -**Issue: "Invalid client credentials"** -```bash -# Solution: Check Keycloak client configuration -- Verify client ID matches environment variable -- Ensure client secret is correct -- Confirm client type is "confidential" -- Check "Direct Access Grants" is enabled -``` - -**Issue: "CORS errors during login"** -```bash -# Solution: Configure Keycloak CORS settings -- In Keycloak Admin Console -- Go to Client โ†’ Settings โ†’ Advanced Settings -- Add your domain to "Web Origins" -- Set "Valid Redirect URIs" properly -``` - -**Issue: "Session not persisting after login"** -```typescript -// Solution: Check cookie configuration -setCookie(event, 'session-name', sessionId, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', // Important! - sameSite: 'lax', - domain: process.env.NODE_ENV === 'production' ? '.yourdomain.com' : undefined -}); -``` - -**Issue: "Redirect loop after login"** -```typescript -// Solution: Check middleware configuration -// Ensure pages have correct middleware: -- Login page: middleware: 'guest' -- Dashboard pages: middleware: 'auth' -- Index page: client-side navigation only -``` - -**Issue: "User groups/roles not being extracted"** -```typescript -// Solution: Configure group mappers in Keycloak -// 1. Go to Client Scopes โ†’ roles โ†’ Mappers -// 2. Create mapper: "groups" -// 3. Mapper Type: "Group Membership" -// 4. Token Claim Name: "groups" -// 5. Add to access token and ID token -``` - -### **2. Debug Commands** - -```bash -# Check Keycloak connectivity -curl -X GET "https://auth.yourdomain.com/realms/your-realm/.well-known/openid_configuration" - -# Test token endpoint directly -curl -X POST "https://auth.yourdomain.com/realms/your-realm/protocol/openid-connect/token" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=password&client_id=your-client&client_secret=your-secret&username=test&password=test123" - -# Verify user info endpoint -curl -X GET "https://auth.yourdomain.com/realms/your-realm/protocol/openid-connect/userinfo" \ - -H "Authorization: Bearer YOUR_ACCESS_TOKEN" -``` - -### **3. Common Configuration Mistakes** - -โŒ **Wrong Grant Type:** -```javascript -// WRONG - Don't use authorization code flow for custom login -standardFlowEnabled: true -directAccessGrantsEnabled: false -``` - -โœ… **Correct Grant Type:** -```javascript -// CORRECT - Use Resource Owner Password Credentials -standardFlowEnabled: false -directAccessGrantsEnabled: true -``` - -โŒ **Wrong Cookie Settings:** -```typescript -// WRONG - Insecure cookie settings -setCookie(event, 'session', sessionId, { - httpOnly: false, // โŒ XSS vulnerable - secure: false, // โŒ Not secure over HTTP - sameSite: 'none' // โŒ CSRF vulnerable -}); -``` - -โœ… **Correct Cookie Settings:** -```typescript -// CORRECT - Secure cookie settings -setCookie(event, 'session', sessionId, { - httpOnly: true, // โœ… XSS protection - secure: isProduction, // โœ… HTTPS in production - sameSite: 'lax' // โœ… CSRF protection -}); -``` - -### **4. Performance Troubleshooting** - -**Slow Login Response:** -- Check network latency to Keycloak server -- Verify Keycloak server performance -- Consider caching user info for short periods -- Optimize session creation process - -**High Memory Usage:** -- Implement session cleanup for expired sessions -- Use external session storage (Redis) instead of memory -- Set appropriate session TTL values - -**Database Connection Issues:** -- Configure proper connection pooling -- Set appropriate timeout values -- Implement retry logic for transient failures - -## ๐Ÿ“š **Additional Resources** - -### **Keycloak Documentation** -- [Resource Owner Password Credentials Flow](https://www.keycloak.org/docs/latest/securing_apps/#_resource_owner_password_credentials_flow) -- [Client Configuration](https://www.keycloak.org/docs/latest/server_admin/#_clients) -- [User Groups and Roles](https://www.keycloak.org/docs/latest/server_admin/#con-groups_server_administration_guide) - -### **Security Best Practices** -- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) -- [OWASP Session Management](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) -- [Cookie Security Guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#security) - -### **Nuxt 3 Specific** -- [Nuxt 3 Authentication Patterns](https://nuxt.com/docs/guide/directory-structure/middleware#authentication) -- [Server Routes](https://nuxt.com/docs/guide/directory-structure/server) -- [Runtime Config](https://nuxt.com/docs/api/nuxt-config#runtimeconfig) - ---- - -## ๐ŸŽ‰ **Conclusion** - -This comprehensive guide covers implementing a custom login page with Keycloak in a Nuxt 3 application. The solution provides: - -โœ… **Complete UI Control**: Custom branded login experience -โœ… **Mobile Compatibility**: Works perfectly on all devices including iOS Safari -โœ… **Enterprise Security**: Rate limiting, input validation, secure sessions -โœ… **Production Ready**: Proper error handling, monitoring, and scalability -โœ… **Maintainable**: Clean separation of concerns and well-documented code - -The implementation balances security, performance, and user experience while providing a solid foundation for enterprise authentication needs. diff --git a/LOGIN_LOOP_FIX_FINAL.md b/LOGIN_LOOP_FIX_FINAL.md deleted file mode 100644 index 3c6cd13..0000000 --- a/LOGIN_LOOP_FIX_FINAL.md +++ /dev/null @@ -1,134 +0,0 @@ -# ๐Ÿ”ง Login Loop Fix - Complete Solution - -## ๐Ÿšจ **Problem Analysis** - -**Sequential Thinking Diagnosis:** -- Desktop login was just reloading the login page -- Mobile still had endless login loops -- Root cause: Session check throttling was too aggressive -- The 5-second throttle prevented login verification from working - -## ๐Ÿ” **Root Cause Found** - -### **Login Flow Breakdown:** -1. User submits credentials โ†’ Server login succeeds โœ… -2. Client calls `checkAuth()` up to 3 times to verify session -3. **THROTTLING PROBLEM**: All `checkAuth()` calls returned cached `false` -4. Login method thought session failed โ†’ User stayed on login page -5. **Mobile Loop**: Navigation attempts triggered middleware โ†’ More throttled calls โ†’ Endless loop - -## โœ… **Solution Implemented** - -### **Smart Throttling with Force Parameter** - -**1. Enhanced checkAuth() Function** -```typescript -const checkAuth = async (force = false) => { - const now = Date.now(); - - // Allow forced checks to bypass throttling for critical operations - if (!force && now - lastSessionCheck.value < SESSION_CHECK_THROTTLE) { - console.log('๐Ÿšซ Session check throttled, using cached result'); - return !!user.value; - } - - // ... perform actual session check -} -``` - -**2. Updated Login Method** -```typescript -// Force bypass throttling during login verification -sessionSuccess = await checkAuth(true); -``` - -**3. Updated Auth Middleware** -```typescript -// Force check when user is not loaded -if (!user.value) { - await checkAuth(true); // Bypass throttling for middleware -} -``` - -**4. Updated Login Page** -```typescript -// Force check to ensure accurate authentication status on page load -const isAlreadyAuthenticated = await checkAuth(true); -``` - -## ๐ŸŽฏ **How This Fixes Both Issues** - -### **Prevents Login Failure:** -- โœ… Login verification uses `checkAuth(true)` - bypasses throttling -- โœ… Session is properly verified after server login -- โœ… User successfully redirects to dashboard - -### **Prevents Session Spam:** -- โœ… General/repeated calls still use throttling: `checkAuth()` -- โœ… Only critical operations use forced checks: `checkAuth(true)` -- โœ… Overall session API calls dramatically reduced - -### **Prevents Mobile Loops:** -- โœ… Middleware uses forced checks when needed -- โœ… Login page uses accurate authentication status -- โœ… Navigation loops eliminated - -## ๐Ÿ“Š **Expected Server Log Changes** - -### **Before Fix:** -``` -๐Ÿ” Session check requested at: 2025-08-07T14:12:56.288Z -๐Ÿ” Session check requested at: 2025-08-07T14:12:56.328Z -๐Ÿ” Session check requested at: 2025-08-07T14:12:56.367Z -... (50+ checks in 30 seconds) -``` - -### **After Fix:** -``` -๐Ÿ”„ Performing forced session check... (login) -๐Ÿ”„ Performing forced session check... (middleware) -๐Ÿšซ Session check throttled, using cached result (general calls) -... (5-10 checks in 30 seconds) -``` - -## ๐Ÿงช **Testing Checklist** - -### **Desktop Testing:** -- [ ] Login with valid credentials โ†’ Should redirect to dashboard -- [ ] Invalid credentials โ†’ Should show error message -- [ ] Already authenticated โ†’ Should redirect to dashboard immediately - -### **Mobile Testing (iOS Safari):** -- [ ] Login flow works without loops -- [ ] No endless session check spam in server logs -- [ ] Smooth navigation between login and dashboard -- [ ] Back button behavior is correct - -### **Server Monitoring:** -- [ ] Reduced session API call frequency -- [ ] "Forced" vs "throttled" session checks in logs -- [ ] No more 50+ session checks per 30 seconds - -## ๐ŸŽฏ **Key Benefits** - -1. **๐Ÿ”“ Login Works**: Force parameter allows critical auth operations to bypass throttling -2. **๐Ÿ“ฑ Mobile Fixed**: iOS Safari loops eliminated with proper session verification -3. **โšก Performance**: Session spam reduced by 80-90% through smart throttling -4. **๐Ÿ›ก๏ธ Robust**: Critical operations (login, middleware) always work regardless of throttling -5. **๐Ÿ”ง Maintainable**: Clear separation between forced and throttled checks - -## ๐Ÿš€ **Files Modified** - -- `composables/useAuth.ts` - Added force parameter and smart throttling -- `middleware/auth.ts` - Use forced checks for middleware -- `pages/login.vue` - Use forced check for auth status verification -- Removed problematic system metrics entirely - -## ๐Ÿ“ˆ **Success Metrics** - -- **Before**: Login broken on desktop + mobile loops -- **After**: Login works smoothly on both platforms -- **Session Calls**: Reduced from 50+ to <10 per 30 seconds -- **User Experience**: Seamless authentication flow - -The login loop issue should now be **completely resolved** with this targeted smart throttling approach! ๐ŸŽ‰ diff --git a/MEMBER_CREATION_FIX_SUMMARY.md b/MEMBER_CREATION_FIX_SUMMARY.md deleted file mode 100644 index 58d9844..0000000 --- a/MEMBER_CREATION_FIX_SUMMARY.md +++ /dev/null @@ -1,134 +0,0 @@ -# Member Creation Fix Summary - -## Issue Description - -The "Add Member" functionality was failing with validation errors, despite the form appearing to be filled out correctly. The server logs showed: - -``` -Validation failed: First Name is required and must be at least 2 characters, Last Name is required and must be at least 2 characters, Valid email address is required -``` - -The server was receiving field names like `'First Name'`, `'Last Name'`, `'Email'` instead of the expected snake_case names like `first_name`, `last_name`, `email`. - -## Root Cause Analysis - -1. **Field Name Mismatch**: The client form was using display names with spaces, but the server validation expected snake_case field names. -2. **Data Transformation Issue**: Although the client had transformation logic, the server was still receiving the display names. -3. **Empty Field Values**: The validation indicated that required fields were empty or invalid. - -## Implemented Fixes - -### 1. Server-Side Field Mapping (Immediate Fix) - -**File**: `server/api/members/index.post.ts` - -Added a robust field mapping function that handles both display names and snake_case: - -```javascript -function normalizeFieldNames(data: any): any { - const fieldMap: Record = { - 'First Name': 'first_name', - 'Last Name': 'last_name', - 'Email': 'email', - 'Phone': 'phone', - 'Date of Birth': 'date_of_birth', - 'Nationality': 'nationality', - 'Address': 'address', - 'Membership Status': 'membership_status', - 'Member Since': 'member_since', - 'Current Year Dues Paid': 'current_year_dues_paid', - 'Membership Date Paid': 'membership_date_paid', - 'Payment Due Date': 'payment_due_date' - }; - - const normalized: any = {}; - for (const [key, value] of Object.entries(data)) { - const normalizedKey = fieldMap[key] || key; - normalized[normalizedKey] = value; - } - return normalized; -} -``` - -### 2. Enhanced Server Logging - -Added comprehensive logging to track: -- Raw request body data -- Field mapping transformations -- Validation process -- Data sanitization steps - -```javascript -console.log('[api/members.post] Raw body data:', JSON.stringify(body, null, 2)); -console.log('[api/members.post] Normalized fields:', Object.keys(normalizedBody)); -``` - -### 3. Client-Side Debug Enhancement - -**File**: `components/AddMemberDialog.vue` - -Added detailed debugging to identify form data issues: - -```javascript -console.log('[AddMemberDialog] Current form.value:', JSON.stringify(form.value, null, 2)); -console.log('[AddMemberDialog] Field access test:'); -console.log(' - First Name:', form.value['First Name']); -console.log(' - Last Name:', form.value['Last Name']); -console.log('[AddMemberDialog] Transformed memberData:', JSON.stringify(memberData, null, 2)); -``` - -### 4. Syntax Error Fix - -**File**: `server/api/members/[id].put.ts` - -Fixed extra character in import statement: -```javascript -// Before: iimport { updateMember, ... } -// After: import { updateMember, ... } -``` - -## Implementation Strategy - -1. **Immediate Protection**: Server-side field mapping ensures the API works regardless of client field names -2. **Debugging Capability**: Enhanced logging helps identify any future issues -3. **Backward Compatibility**: The fix handles both display names and snake_case names -4. **Error Prevention**: Comprehensive validation with clear error messages - -## Testing Process - -1. **Server Startup**: Fixed syntax error allowing proper development server startup -2. **Form Submission**: Enhanced logging will show exact data flow during member creation -3. **Field Validation**: Server now properly validates fields regardless of naming convention -4. **Data Integrity**: Proper sanitization and transformation ensure clean data storage - -## Benefits - -1. **Robust Error Handling**: Works with various field naming conventions -2. **Detailed Debugging**: Comprehensive logs for troubleshooting -3. **Backward Compatible**: Doesn't break existing functionality -4. **Future Proof**: Handles both current and legacy field names -5. **Clear Error Messages**: Better user feedback when validation fails - -## Next Steps - -1. **Test Member Creation**: Verify the form now works correctly -2. **Monitor Logs**: Check server and client logs for successful data flow -3. **Remove Debug Logs**: Clean up excessive logging once confirmed working -4. **Document Field Standards**: Establish consistent field naming conventions - -## Files Modified - -- `server/api/members/index.post.ts` - Added field mapping and enhanced logging -- `components/AddMemberDialog.vue` - Added client-side debugging -- `server/api/members/[id].put.ts` - Fixed syntax error - -## Expected Behavior - -After this fix: -1. Member creation form should work correctly -2. Server logs will show successful field mapping -3. Client logs will show proper data transformation -4. New members will be created successfully in the database -5. Form validation will provide clear feedback for any remaining issues - -The system now handles both display field names and snake_case field names, providing robustness against client-side data formatting issues. diff --git a/MEMBER_UNDEFINED_DISPLAY_FIX_COMPREHENSIVE.md b/MEMBER_UNDEFINED_DISPLAY_FIX_COMPREHENSIVE.md deleted file mode 100644 index 650d6ca..0000000 --- a/MEMBER_UNDEFINED_DISPLAY_FIX_COMPREHENSIVE.md +++ /dev/null @@ -1,212 +0,0 @@ -# Member "Undefined" Display Issue - Comprehensive Fix - -## Issue Summary - -Members were being created successfully in NocoDB but displaying as "undefined" in the portal interface. This indicated a data schema mismatch between write and read operations. - -## Root Cause Analysis - -After thorough investigation, the issue was identified as a **field name inconsistency** between: -- **Write operations**: Using snake_case (`first_name`, `last_name`) -- **Read operations**: Expecting snake_case but NocoDB returning display names (`First Name`, `Last Name`) -- **Display logic**: Requiring `FullName` computed from `first_name` + `last_name` - -## Comprehensive Diagnostic System Implemented - -### 1. NocoDB Utility Layer Diagnostics -**File**: `server/utils/nocodb.ts` - -Added detailed logging in `getMembers()` to capture: -- Raw member data structure from NocoDB -- Field names and types -- Values for both snake_case and display name variants - -```typescript -// DIAGNOSTIC: Log raw member data structure -console.log('[nocodb.getMembers] DIAGNOSTIC - Raw member fields from NocoDB:', Object.keys(sampleMember)); -console.log('[nocodb.getMembers] DIAGNOSTIC - first_name value:', sampleMember.first_name); -console.log('[nocodb.getMembers] DIAGNOSTIC - First Name value:', (sampleMember as any)['First Name']); -``` - -### 2. API Layer Diagnostics -**File**: `server/api/members/index.get.ts` - -Enhanced GET endpoint with comprehensive logging: -- Raw data from NocoDB -- Field normalization process -- FullName calculation results -- Final processed member structure - -```typescript -// DIAGNOSTIC: Log processing pipeline -console.log('[api/members.get] DIAGNOSTIC - FullName calculation result:', - `"${sampleProcessed.first_name || ''}" + " " + "${sampleProcessed.last_name || ''}" = "${sampleProcessed.FullName}"`); -``` - -### 3. Client-Side Diagnostics -**File**: `pages/dashboard/member-list.vue` - -Added frontend logging to track: -- API response structure -- Member data received by client -- Field availability and values - -```typescript -// DIAGNOSTIC: Log what we received from API -console.log('[member-list] DIAGNOSTIC - Sample FullName:', `"${sampleMember.FullName}"`); -console.log('[member-list] DIAGNOSTIC - Sample first_name:', `"${sampleMember.first_name}"`); -``` - -## Bidirectional Field Normalization System - -### 1. Read Operations (NocoDB โ†’ Application) -**Function**: `normalizeFieldsFromNocoDB()` - -Maps both display names and snake_case to consistent internal format: - -```typescript -const readFieldMap: Record = { - 'First Name': 'first_name', - 'Last Name': 'last_name', - 'Email': 'email', - // ... handles all field variants - 'first_name': 'first_name', // Pass-through for snake_case - 'last_name': 'last_name', -}; -``` - -### 2. Write Operations (Application โ†’ NocoDB) -**Function**: `normalizeFieldsForNocoDB()` - -Maps internal snake_case to NocoDB expected format: - -```typescript -const writeFieldMap: Record = { - 'first_name': 'First Name', - 'last_name': 'Last Name', - 'email': 'Email', - // ... complete mapping for write operations -}; -``` - -### 3. Robust Fallback System - -Ensures data integrity with multiple fallback layers: - -```typescript -// Ensure required fields exist with fallbacks -normalized.first_name = normalized.first_name || normalized['First Name'] || ''; -normalized.last_name = normalized.last_name || normalized['Last Name'] || ''; -normalized.email = normalized.email || normalized['Email'] || normalized['Email Address'] || ''; -``` - -## Integration Points - -### 1. GET Endpoint Integration -Applied field normalization to all retrieved members: - -```typescript -// Apply field normalization to handle schema mismatches -members = members.map(member => { - const normalized = normalizeFieldsFromNocoDB(member); - return normalized; -}); -``` - -### 2. POST Endpoint Integration -Already included the server-side field mapping from previous fix: - -```typescript -// Map display names to snake_case field names (fallback for client issues) -const normalizedBody = normalizeFieldNames(body); -``` - -## Testing Strategy - -### 1. Server-Side Logging -Check server console for diagnostic output: -- `[nocodb.getMembers] DIAGNOSTIC` - Raw NocoDB data -- `[api/members.get] DIAGNOSTIC` - Processing pipeline -- `[normalizeFieldsFromNocoDB]` - Field mapping results - -### 2. Client-Side Logging -Check browser console for: -- `[member-list] DIAGNOSTIC` - Frontend data reception -- Member field structure and values -- FullName calculation success - -### 3. Manual Testing -1. **Create New Member**: Verify creation and immediate display -2. **Reload Page**: Check if existing members display correctly -3. **Edit Member**: Verify editing functionality works -4. **View Member**: Check detailed view displays properly - -## Expected Results - -After implementing these fixes: - -### โœ… **Successful Scenarios**: -1. **New Members**: Display immediately after creation -2. **Existing Members**: Show correct names instead of "undefined" -3. **Mixed Schemas**: Handle both display names and snake_case data -4. **Robust Fallbacks**: Work regardless of NocoDB column naming - -### ๐Ÿ” **Diagnostic Information**: -- Complete visibility into data flow from database to display -- Identification of exact field naming used by NocoDB -- Validation of field mapping effectiveness -- Confirmation of FullName calculation success - -### ๐Ÿ› ๏ธ **Technical Benefits**: -- **Backward Compatible**: Works with existing data -- **Future Proof**: Handles schema changes gracefully -- **Debuggable**: Comprehensive logging for troubleshooting -- **Maintainable**: Clean separation of concerns - -## Troubleshooting Guide - -### Issue: Still Seeing "Undefined" Names -**Check**: Server logs for `[normalizeFieldsFromNocoDB]` output -**Action**: Verify field mapping covers the actual NocoDB column names - -### Issue: Empty FullName Field -**Check**: `[api/members.get] DIAGNOSTIC - FullName calculation result` -**Action**: Confirm `first_name` and `last_name` have values after normalization - -### Issue: API Errors -**Check**: Server console for `[nocodb.getMembers]` errors -**Action**: Verify NocoDB connection and table configuration - -### Issue: Client Not Receiving Data -**Check**: Browser console for `[member-list] DIAGNOSTIC` logs -**Action**: Confirm API response structure and member data format - -## Files Modified - -### Server-Side Changes: -1. **`server/utils/nocodb.ts`** - - Added diagnostic logging to `getMembers()` - - Implemented `normalizeFieldsFromNocoDB()` - - Implemented `normalizeFieldsForNocoDB()` - -2. **`server/api/members/index.get.ts`** - - Enhanced diagnostic logging - - Integrated field normalization - - Added import for normalization functions - -3. **`server/api/members/index.post.ts`** - - Previous field mapping enhancement (already implemented) - -### Client-Side Changes: -1. **`pages/dashboard/member-list.vue`** - - Added comprehensive client-side diagnostic logging - - Enhanced member data tracking - -## Next Steps - -1. **Deploy and Test**: Apply these changes and monitor logs -2. **Identify Schema**: Use diagnostic output to confirm exact NocoDB field names -3. **Optimize**: Remove excessive logging once issue is resolved -4. **Document**: Update field naming standards based on findings - -This comprehensive fix provides both immediate resolution and long-term robustness for the member display system. diff --git a/PASSWORD_RESET_FIX_SUMMARY.md b/PASSWORD_RESET_FIX_SUMMARY.md deleted file mode 100644 index 3493902..0000000 --- a/PASSWORD_RESET_FIX_SUMMARY.md +++ /dev/null @@ -1,110 +0,0 @@ -# Password Reset Fix - Implementation Summary - -## Problem -The password reset functionality was failing with a 500 error because the portal client (`monacousa-portal`) was being used to access Keycloak's Admin API, but it didn't have the necessary permissions to execute admin operations like sending password reset emails. - -## Root Cause -The original implementation was using the portal client credentials for both: -1. User authentication (correct usage) -2. Admin operations like password reset (incorrect - needs admin permissions) - -Error from logs: -``` -โŒ Failed to send reset email: 500 -Reset email error details: {"errorMessage":"Failed to send execute actions email: Error when attempting to send the email to the server. More information is available in the server log."} -``` - -## Solution -Implemented a dedicated admin client approach using Keycloak's `admin-cli` client: - -### 1. Keycloak Configuration -- Enabled "Client authentication" for `admin-cli` client -- Enabled "Service accounts roles" -- Assigned realm-management roles: - - `view-users` - - `manage-users` - - `query-users` -- Generated client secret - -### 2. Environment Variables -Added new admin client configuration: -```env -NUXT_KEYCLOAK_ADMIN_CLIENT_ID=admin-cli -NUXT_KEYCLOAK_ADMIN_CLIENT_SECRET=your-admin-cli-secret -``` - -### 3. Code Changes - -#### Files Modified: -- `nuxt.config.ts` - Added keycloakAdmin runtime config -- `.env.example` - Documented new environment variables -- `utils/types.ts` - Added KeycloakAdminConfig interface -- `server/utils/keycloak-admin.ts` - **NEW** Admin client utility -- `server/api/auth/forgot-password.post.ts` - Updated to use admin client - -#### Key Fix: -**Before (broken):** -```typescript -// Using portal client for admin operations (no permissions) -body: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: config.keycloak.clientId, // monacousa-portal - client_secret: config.keycloak.clientSecret // portal secret -}) -``` - -**After (working):** -```typescript -// Using admin client for admin operations (has permissions) -body: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: config.keycloakAdmin.clientId, // admin-cli - client_secret: config.keycloakAdmin.clientSecret // admin secret -}) -``` - -### 4. Enhanced Error Handling -Added specific handling for: -- Permission errors (403/Forbidden) -- SMTP server errors (500) -- Timeout errors -- User not found scenarios - -### 5. Security Improvements -- Always return generic success messages (don't reveal if email exists) -- Enhanced logging for debugging -- Proper error categorization -- Rate limiting considerations documented - -## Architecture -``` -Password Reset Flow: -1. User submits email via forgot password form -2. Server validates email format -3. Server creates Keycloak admin client -4. Admin client obtains admin token using admin-cli credentials -5. Admin client searches for user by email -6. If user found, admin client sends password reset email -7. Server always returns generic success message -``` - -## Benefits -- โœ… Password reset emails now work properly -- โœ… Proper separation of concerns (portal vs admin operations) -- โœ… Enhanced security and error handling -- โœ… Better logging for troubleshooting -- โœ… Maintainable admin utility for future admin operations - -## Testing -To test the fix: -1. Navigate to login page -2. Click "Forgot Password" -3. Enter valid email address -4. Check email inbox for reset link -5. Verify server logs show successful operation - -## Future Enhancements -- Rate limiting on forgot password endpoint -- CAPTCHA integration -- Admin dashboard for user management -- Email template customization diff --git a/REDIRECT_LOOP_SOLUTION_FINAL.md b/REDIRECT_LOOP_SOLUTION_FINAL.md deleted file mode 100644 index dfa4626..0000000 --- a/REDIRECT_LOOP_SOLUTION_FINAL.md +++ /dev/null @@ -1,222 +0,0 @@ -# ๐ŸŽฏ REDIRECT LOOP SOLUTION - COMPREHENSIVE FIX - -## ๐Ÿšจ **THE PROBLEM** - -**Endless redirect loop** between `/login` โ†” `/dashboard` caused by multiple conflicting auth checks running simultaneously and SSR/hydration mismatches. - -## ๐Ÿ” **ROOT CAUSE ANALYSIS** - -### **The Critical Issues Found:** - -1. **`pages/index.vue`**: Top-level `await navigateTo()` caused SSR/hydration issues -2. **Multiple Auth Checks**: Same auth state being checked in plugins, middleware, and onMounted hooks -3. **Race Conditions**: Plugin, middleware, and component lifecycle all checking auth simultaneously -4. **SSR Mismatches**: Server and client had different auth states during hydration -5. **Missing Middleware**: Login page didn't use guest middleware to handle authenticated users - -### **The Redirect Loop Flow:** -1. User visits site โ†’ `index.vue` top-level navigation (SSR issues) -2. Plugin checks auth โ†’ Sets user state -3. Login page `onMounted` checks auth โ†’ Finds user โ†’ Redirects to `/dashboard` -4. Dashboard middleware checks auth โ†’ User might not be set yet โ†’ Redirects to `/login` -5. **INFINITE LOOP** ๐Ÿ”„ - -## โœ… **THE COMPLETE SOLUTION** - -### **1. Fixed `pages/index.vue` - Eliminated SSR Issues** -```vue - - - -``` - -**Why this works:** -- โŒ No top-level async operations that break SSR -- โœ… Client-side only navigation prevents hydration mismatches -- โœ… Clean loading state while routing decision is made - -### **2. Fixed Login Page - Proper Middleware Usage** -```vue - -``` - -**Why this works:** -- โœ… Guest middleware handles authenticated user redirects properly -- โŒ No conflicting auth checks in onMounted -- โœ… Single responsibility: middleware for auth, component for UI - -### **3. Fixed Dashboard Index - Removed Duplicate Checks** -```vue - -``` - -**Why this works:** -- โœ… Auth middleware ensures user is authenticated before component loads -- โŒ No duplicate auth checks that could conflict -- โœ… Simple tier-based routing logic - -### **4. Made Auth State SSR-Compatible** -```typescript -// composables/useAuth.ts -export const useAuth = () => { - // โœ… CHANGED: Use useState for SSR compatibility - const user = useState('auth.user', () => null); - - // โŒ OLD: const user = ref(null); - // This caused hydration mismatches between server/client -``` - -**Why this works:** -- โœ… `useState` ensures consistent state between server and client -- โŒ Prevents hydration mismatches that caused loops -- โœ… Proper SSR/SPA compatibility - -## ๐ŸŽฏ **KEY PRINCIPLES APPLIED** - -### **1. Single Responsibility** -- **Middleware**: Handles auth checks and redirects -- **Components**: Handle UI and user interactions only -- **Plugins**: Initialize auth state on app startup - -### **2. Eliminate Race Conditions** -- โŒ No multiple auth checks running simultaneously -- โœ… Clear order: Plugin โ†’ Middleware โ†’ Component lifecycle -- โœ… Each layer trusts the previous layer's work - -### **3. SSR Compatibility** -- โŒ No top-level async operations in components -- โœ… Use `onMounted` for client-side only operations -- โœ… Use `useState` for consistent server/client state - -### **4. Proper Middleware Usage** -- **Login page**: Uses `guest` middleware (redirects authenticated users) -- **Dashboard pages**: Use `auth` middleware (redirects unauthenticated users) -- **No conflicting checks** in component lifecycle hooks - -## ๐Ÿ“Š **THE AUTHENTICATION FLOW NOW** - -### **Happy Path - User Logs In:** -1. **Visit `/`** โ†’ Loading screen โ†’ Routes to `/login` (if not authenticated) -2. **Login Page** โ†’ Guest middleware allows access โ†’ User enters credentials -3. **Login Success** โ†’ Server sets cookie โ†’ Routes to `/dashboard` -4. **Dashboard Index** โ†’ Auth middleware verifies โ†’ Routes to `/dashboard/user` -5. **User Dashboard** โ†’ Loads successfully โœ… - -### **Already Authenticated User:** -1. **Plugin** โ†’ Checks auth โ†’ Sets user state -2. **Visit `/`** โ†’ Routes to `/dashboard` -3. **Dashboard Index** โ†’ Auth middleware passes โ†’ Routes to `/dashboard/user` -4. **User Dashboard** โ†’ Loads successfully โœ… - -### **Unauthenticated User Tries Dashboard:** -1. **Visit `/dashboard`** โ†’ Auth middleware โ†’ Redirects to `/login` -2. **Login Page** โ†’ Guest middleware allows access -3. **User can login** โœ… - -### **Authenticated User Visits Login:** -1. **Visit `/login`** โ†’ Guest middleware โ†’ Redirects to `/dashboard` -2. **Dashboard loads** โœ… - -## ๐ŸŽ‰ **WHAT THIS FIXES** - -### **Before Fix โŒ** -- Endless redirect loops between login/dashboard -- White screens during navigation -- SSR/hydration mismatches -- Race conditions between auth checks -- Inconsistent behavior across devices - -### **After Fix โœ…** -- Clean, predictable auth flow -- No redirect loops -- Proper SSR/SPA compatibility -- Single source of truth for auth state -- Consistent behavior across all platforms - -## ๐Ÿ“‹ **FILES MODIFIED** - -1. **`pages/index.vue`** - Removed top-level navigation, added client-side routing -2. **`pages/login.vue`** - Added guest middleware, removed duplicate auth check -3. **`pages/dashboard/index.vue`** - Removed duplicate auth check, simplified routing -4. **`composables/useAuth.ts`** - Changed to useState for SSR compatibility - -## ๐Ÿ”ง **TESTING CHECKLIST** - -### **Desktop Testing:** -- [ ] Visit `/` โ†’ Should load and route properly -- [ ] Login with valid credentials โ†’ Should redirect to dashboard -- [ ] Already authenticated โ†’ Should skip login page -- [ ] Unauthenticated dashboard access โ†’ Should redirect to login - -### **Mobile Testing:** -- [ ] All above scenarios work on mobile browsers -- [ ] No redirect loops in iOS Safari -- [ ] Smooth navigation between pages - -### **Server Logs:** -- [ ] No excessive session API calls -- [ ] Clean authentication flow logs -- [ ] No error messages about hydration - -## ๐ŸŽฏ **SUCCESS CRITERIA** - -โœ… **No more redirect loops between `/login` and `/dashboard`** -โœ… **Clean authentication flow on all devices** -โœ… **Proper SSR/SPA compatibility** -โœ… **Consistent user experience** -โœ… **Maintainable, single-responsibility code** - -The authentication system now works reliably with a clear, predictable flow that eliminates all race conditions and conflicts! ๐Ÿš€ diff --git a/SNAKE_CASE_FIELD_MIGRATION_GUIDE.md b/SNAKE_CASE_FIELD_MIGRATION_GUIDE.md deleted file mode 100644 index d6ccfd7..0000000 --- a/SNAKE_CASE_FIELD_MIGRATION_GUIDE.md +++ /dev/null @@ -1,187 +0,0 @@ -# MonacoUSA Portal - Snake Case Field Migration Guide - -## ๐ŸŽฏ Overview - -This document provides complete instructions for migrating from space-separated field names (e.g., "First Name") to snake_case field names (e.g., "first_name") to eliminate data corruption issues when editing records directly in NocoDB. - -## ๐Ÿ“Š Required NocoDB Field Name Changes - -You need to rename the following fields in your NocoDB Members table: - -| **Current Field Name** | **New Snake Case Name** | **Type** | -|----------------------------|----------------------------|-------------| -| `First Name` | `first_name` | Text | -| `Last Name` | `last_name` | Text | -| `Email` | `email` | Email | -| `Phone` | `phone` | Text | -| `Date of Birth` | `date_of_birth` | Date | -| `Nationality` | `nationality` | Text | -| `Address` | `address` | LongText | -| `Membership Status` | `membership_status` | SingleSelect| -| `Member Since` | `member_since` | Date | -| `Current Year Dues Paid` | `current_year_dues_paid` | Text | -| `Membership Date Paid` | `membership_date_paid` | Date | -| `Payment Due Date` | `payment_due_date` | Date | - -### ๐Ÿ”ง How to Rename Fields in NocoDB - -1. **Open your NocoDB Members table** -2. **For each field above:** - - Click on the field header - - Select "Edit" or click the gear icon - - Change the field name from the old name to the new snake_case name - - Click "Save" - -โš ๏ธ **Important**: Do NOT change the field types, only the names. - -## ๐Ÿ—๏ธ Backend Files Updated - -The following files have been completely updated to use snake_case field names: - -### Type Definitions -- โœ… `utils/types.ts` - Member interface updated - -### NocoDB Utilities -- โœ… `server/utils/nocodb.ts` - All CRUD operations updated - -### API Endpoints -- โœ… `server/api/members/index.get.ts` - List members API -- โœ… `server/api/members/[id].get.ts` - Get single member API -- โœ… `server/api/members/index.post.ts` - Create member API -- โœ… `server/api/members/[id].put.ts` - Update member API - -## ๐ŸŽจ Frontend Files That Need Updates - -The following frontend components still reference the old field names and need to be updated: - -### Vue Components -- `components/ViewMemberDialog.vue` -- `components/MemberCard.vue` -- `components/EditMemberDialog.vue` -- `components/AddMemberDialog.vue` -- `pages/dashboard/member-list.vue` - -## ๐Ÿ”„ Complete Field Mapping Reference - -### Data Access Patterns - -**Before (โŒ Old)**: -```javascript -member['First Name'] -member['Last Name'] -member['Membership Status'] -member['Current Year Dues Paid'] -``` - -**After (โœ… New)**: -```javascript -member.first_name -member.last_name -member.membership_status -member.current_year_dues_paid -``` - -### API Request/Response Format - -**Create/Update Member Payload**: -```json -{ - "first_name": "John", - "last_name": "Doe", - "email": "john.doe@example.com", - "phone": "+1234567890", - "nationality": "US", - "membership_status": "Active", - "current_year_dues_paid": "true", - "date_of_birth": "1990-01-15", - "member_since": "2023-01-01", - "membership_date_paid": "2024-01-15", - "payment_due_date": "2025-01-15", - "address": "123 Main St, City, State" -} -``` - -## ๐Ÿงช Testing Checklist - -After making the NocoDB field changes, test the following: - -### Backend API Testing -- [ ] `GET /api/members` - List all members -- [ ] `GET /api/members/{id}` - Get single member -- [ ] `POST /api/members` - Create new member -- [ ] `PUT /api/members/{id}` - Update existing member -- [ ] `DELETE /api/members/{id}` - Delete member - -### Data Integrity Testing -- [ ] Create member via portal โ†’ Verify in NocoDB -- [ ] Edit member via portal โ†’ Verify in NocoDB -- [ ] Edit member in NocoDB directly โ†’ Verify portal displays correctly -- [ ] All fields display properly (names, flags, contact info) - -### Frontend Display Testing -- [ ] Member list page loads and displays all members -- [ ] Member cards show correct information -- [ ] Country flags display properly -- [ ] Search and filtering work correctly -- [ ] Add member dialog works -- [ ] Edit member dialog works and pre-populates correctly -- [ ] View member dialog shows all details - -## ๐Ÿšจ Critical Success Criteria - -The migration is successful when: - -1. โœ… **No "undefined undefined" names appear** in member cards -2. โœ… **Country flags display properly** for all nationalities -3. โœ… **Direct NocoDB edits sync properly** with portal display -4. โœ… **All CRUD operations work** through the portal -5. โœ… **No TypeScript errors** in the console -6. โœ… **No API errors** in browser network tab - -## ๐Ÿ”ง Rollback Plan - -If issues occur, you can temporarily rollback by: - -1. **Revert NocoDB field names** back to the original space-separated format -2. **Revert the backend files** using git: - ```bash - git checkout HEAD~1 -- utils/types.ts server/utils/nocodb.ts server/api/members/ - ``` - -## ๐Ÿ“ˆ Next Steps After Migration - -1. **Update frontend components** to use snake_case field names -2. **Test thoroughly** across all functionality -3. **Update any documentation** that references old field names -4. **Consider adding validation** to prevent future field name inconsistencies - -## ๐Ÿ’ก Benefits After Migration - -- โœ… **Consistent data display** regardless of edit source (portal vs NocoDB) -- โœ… **No more "undefined undefined" member names** -- โœ… **Proper country flag rendering** -- โœ… **Standard database naming conventions** -- โœ… **Easier debugging and maintenance** -- โœ… **Better API consistency** - -## ๐Ÿ†˜ Troubleshooting - -### Issue: Members showing "undefined undefined" -- **Cause**: NocoDB field names don't match backend expectations -- **Solution**: Verify all field names in NocoDB match the snake_case format exactly - -### Issue: Country flags not displaying -- **Cause**: Nationality field not properly mapped -- **Solution**: Ensure `Nationality` field is renamed to `nationality` in NocoDB - -### Issue: API errors in console -- **Cause**: Field name mismatch between frontend and backend -- **Solution**: Update frontend components to use snake_case field names - -### Issue: Direct NocoDB edits causing corruption -- **Cause**: This was the original problem - should be fixed after migration -- **Solution**: This migration specifically addresses this issue - ---- - -**๐ŸŽ‰ Once you complete the NocoDB field renaming, the backend will be fully compatible with snake_case field names and the data corruption issue should be completely resolved!** diff --git a/components/AddMemberDialog.vue b/components/AddMemberDialog.vue index f08656f..e54e071 100644 --- a/components/AddMemberDialog.vue +++ b/components/AddMemberDialog.vue @@ -322,43 +322,47 @@ const handleSubmit = async () => { console.log('[AddMemberDialog] Form keys:', Object.keys(form.value)); console.log('[AddMemberDialog] duesPaid switch value:', duesPaid.value); - // Transform field names to match server expectations (snake_case) + // Get current form values + const currentForm = unref(form); + + console.log('[AddMemberDialog] Unref form access test:'); + console.log(' - First Name:', currentForm['First Name']); + console.log(' - Last Name:', currentForm['Last Name']); + console.log(' - Email:', currentForm.Email); + console.log(' - Phone:', currentForm.Phone); + + // Simple approach - send the form data as-is with display names + // Let the server handle field normalization const memberData = { - first_name: form.value['First Name']?.trim(), - last_name: form.value['Last Name']?.trim(), - email: form.value.Email?.trim(), - phone: form.value.Phone?.trim() || null, - date_of_birth: form.value['Date of Birth'] || null, - nationality: form.value.Nationality?.trim() || null, - address: form.value.Address?.trim() || null, - membership_status: form.value['Membership Status'], - member_since: form.value['Member Since'] || null, - current_year_dues_paid: form.value['Current Year Dues Paid'], - membership_date_paid: form.value['Membership Date Paid'] || null, - payment_due_date: form.value['Payment Due Date'] || null + 'First Name': currentForm['First Name']?.trim(), + 'Last Name': currentForm['Last Name']?.trim(), + 'Email': currentForm.Email?.trim(), + 'Phone': currentForm.Phone?.trim() || '', + 'Date of Birth': currentForm['Date of Birth'] || '', + 'Nationality': currentForm.Nationality?.trim() || '', + 'Address': currentForm.Address?.trim() || '', + 'Membership Status': currentForm['Membership Status'], + 'Member Since': currentForm['Member Since'] || '', + 'Current Year Dues Paid': currentForm['Current Year Dues Paid'], + 'Membership Date Paid': currentForm['Membership Date Paid'] || '', + 'Payment Due Date': currentForm['Payment Due Date'] || '' }; - - console.log('[AddMemberDialog] Field access test:'); - console.log(' - First Name:', form.value['First Name']); - console.log(' - Last Name:', form.value['Last Name']); - console.log(' - Email:', form.value.Email); - console.log(' - Phone:', form.value.Phone); // Ensure required fields are not empty - if (!memberData.first_name) { - console.error('[AddMemberDialog] First Name is empty. Raw value:', form.value['First Name']); + if (!memberData['First Name']) { + console.error('[AddMemberDialog] First Name is empty. Raw value:', currentForm['First Name']); throw new Error('First Name is required'); } - if (!memberData.last_name) { - console.error('[AddMemberDialog] Last Name is empty. Raw value:', form.value['Last Name']); + if (!memberData['Last Name']) { + console.error('[AddMemberDialog] Last Name is empty. Raw value:', currentForm['Last Name']); throw new Error('Last Name is required'); } - if (!memberData.email) { - console.error('[AddMemberDialog] Email is empty. Raw value:', form.value.Email); + if (!memberData['Email']) { + console.error('[AddMemberDialog] Email is empty. Raw value:', currentForm.Email); throw new Error('Email is required'); } - console.log('[AddMemberDialog] Transformed memberData:', JSON.stringify(memberData, null, 2)); + console.log('[AddMemberDialog] Final memberData:', JSON.stringify(memberData, null, 2)); console.log('[AddMemberDialog] About to submit to API...'); const response = await $fetch<{ success: boolean; data: Member; message?: string }>('/api/members', { diff --git a/components/EnhancedPhoneInput.vue b/components/EnhancedPhoneInput.vue deleted file mode 100644 index f3530e8..0000000 --- a/components/EnhancedPhoneInput.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - - - diff --git a/components/MultipleNationalityInput.vue b/components/MultipleNationalityInput.vue index b4e5dee..223793a 100644 --- a/components/MultipleNationalityInput.vue +++ b/components/MultipleNationalityInput.vue @@ -1,7 +1,5 @@