Replace date-fns with native date formatting and remove unused code
Build And Push Image / docker (push) Successful in 1m34s
Details
Build And Push Image / docker (push) Successful in 1m34s
Details
Remove date-fns dependency in favor of native Intl.DateTimeFormat APIs, clean up obsolete admin endpoints, utility files, and archived documentation. Consolidate docs structure and remove unused plugins.
This commit is contained in:
parent
676bbc04f6
commit
503d68cd2d
|
|
@ -334,7 +334,36 @@
|
||||||
import type { Event, EventRSVP } from '~/utils/types';
|
import type { Event, EventRSVP } from '~/utils/types';
|
||||||
import { useEvents } from '~/composables/useEvents';
|
import { useEvents } from '~/composables/useEvents';
|
||||||
import { useAuth } from '~/composables/useAuth';
|
import { useAuth } from '~/composables/useAuth';
|
||||||
import { format } from 'date-fns';
|
// Helper function to replace date-fns format
|
||||||
|
const formatDate = (date: Date, formatStr: string): string => {
|
||||||
|
if (formatStr === 'EEEE, MMMM d, yyyy') {
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
} else if (formatStr === 'MMM d') {
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
} else if (formatStr === 'MMM d, yyyy') {
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
} else if (formatStr === 'HH:mm') {
|
||||||
|
return date.toLocaleTimeString('en-US', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
|
|
@ -431,9 +460,9 @@ const formatEventDate = computed(() => {
|
||||||
const endDate = new Date(props.event.end_datetime);
|
const endDate = new Date(props.event.end_datetime);
|
||||||
|
|
||||||
if (startDate.toDateString() === endDate.toDateString()) {
|
if (startDate.toDateString() === endDate.toDateString()) {
|
||||||
return format(startDate, 'EEEE, MMMM d, yyyy');
|
return formatDate(startDate, 'EEEE, MMMM d, yyyy');
|
||||||
} else {
|
} else {
|
||||||
return `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d, yyyy')}`;
|
return `${formatDate(startDate, 'MMM d')} - ${formatDate(endDate, 'MMM d, yyyy')}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -442,7 +471,7 @@ const formatEventTime = computed(() => {
|
||||||
const startDate = new Date(props.event.start_datetime);
|
const startDate = new Date(props.event.start_datetime);
|
||||||
const endDate = new Date(props.event.end_datetime);
|
const endDate = new Date(props.event.end_datetime);
|
||||||
|
|
||||||
return `${format(startDate, 'HH:mm')} - ${format(endDate, 'HH:mm')}`;
|
return `${formatDate(startDate, 'HH:mm')} - ${formatDate(endDate, 'HH:mm')}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const capacityPercentage = computed(() => {
|
const capacityPercentage = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,18 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getAllCountries, searchCountries } from '~/utils/countries';
|
import { getAllCountries, searchCountries } from '~/utils/countries';
|
||||||
import { getStaticDeviceInfo } from '~/utils/static-device-detection';
|
// Simple device detection utilities
|
||||||
|
const detectMobile = () => {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectMobileSafari = () => {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
return /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: string; // Comma-separated string like "FR,MC,US"
|
modelValue?: string; // Comma-separated string like "FR,MC,US"
|
||||||
|
|
@ -241,11 +252,19 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
// Static mobile detection (no reactive dependencies)
|
// Device detection
|
||||||
const deviceInfo = getStaticDeviceInfo();
|
const isMobile = ref(false);
|
||||||
const isMobile = ref(deviceInfo.isMobile);
|
const isMobileSafari = ref(false);
|
||||||
const isMobileSafari = ref(deviceInfo.isMobileSafari);
|
const needsPerformanceMode = ref(false);
|
||||||
const needsPerformanceMode = ref(deviceInfo.isMobileSafari || deviceInfo.isMobile);
|
|
||||||
|
// Initialize device detection on mount
|
||||||
|
onMounted(() => {
|
||||||
|
if (process.client) {
|
||||||
|
isMobile.value = detectMobile();
|
||||||
|
isMobileSafari.value = detectMobileSafari();
|
||||||
|
needsPerformanceMode.value = isMobileSafari.value || isMobile.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Parse initial nationalities from comma-separated string
|
// Parse initial nationalities from comma-separated string
|
||||||
const parseNationalities = (value: string): string[] => {
|
const parseNationalities = (value: string): string[] => {
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { parsePhoneNumber, AsYouType } from 'libphonenumber-js';
|
import { parsePhoneNumber, AsYouType } from 'libphonenumber-js';
|
||||||
import { getPhoneCountriesWithPreferred, searchPhoneCountries, getPhoneCountryByCode, type PhoneCountry } from '~/utils/phone-countries';
|
import { getPhoneCountriesWithPreferred, searchPhoneCountries, getPhoneCountryByCode, type PhoneCountry } from '~/utils/phone-countries';
|
||||||
import { getStaticDeviceInfo } from '~/utils/static-device-detection';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: string;
|
modelValue?: string;
|
||||||
|
|
@ -188,10 +187,18 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
// Static mobile detection (no reactive dependencies)
|
// Simple mobile detection
|
||||||
const deviceInfo = getStaticDeviceInfo();
|
const isMobile = ref(false);
|
||||||
const isMobile = ref(deviceInfo.isMobile);
|
const isMobileSafari = ref(false);
|
||||||
const isMobileSafari = ref(deviceInfo.isMobileSafari);
|
|
||||||
|
// Initialize mobile detection
|
||||||
|
onMounted(() => {
|
||||||
|
if (process.client) {
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
isMobile.value = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
||||||
|
isMobileSafari.value = /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Create computed-like object for template compatibility
|
// Create computed-like object for template compatibility
|
||||||
const mobileDetection = computed(() => ({
|
const mobileDetection = computed(() => ({
|
||||||
|
|
@ -326,12 +333,11 @@ watch(dropdownOpen, (isOpen) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Component initialization - values already set from static detection
|
// Component initialization
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Device detection already applied statically - no additional setup needed
|
|
||||||
console.log('[PhoneInputWrapper] Initialized with device info:', {
|
console.log('[PhoneInputWrapper] Initialized with device info:', {
|
||||||
isMobile: deviceInfo.isMobile,
|
isMobile: isMobile.value,
|
||||||
isMobileSafari: deviceInfo.isMobileSafari
|
isMobileSafari: isMobileSafari.value
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,42 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Event, EventRSVP } from '~/utils/types';
|
import type { Event, EventRSVP } from '~/utils/types';
|
||||||
import { format, isWithinInterval, addDays } from 'date-fns';
|
|
||||||
|
// Helper functions to replace date-fns
|
||||||
|
const formatDate = (date: Date, formatStr: string): string => {
|
||||||
|
const options: Intl.DateTimeFormatOptions = {};
|
||||||
|
|
||||||
|
if (formatStr === 'HH:mm') {
|
||||||
|
options.hour = '2-digit';
|
||||||
|
options.minute = '2-digit';
|
||||||
|
options.hour12 = false;
|
||||||
|
} else if (formatStr === 'EEE, MMM d • HH:mm') {
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
weekday: 'short',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
}) + ' • ' + date.toLocaleTimeString('en-US', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
} else if (formatStr === 'MMM d') {
|
||||||
|
options.month = 'short';
|
||||||
|
options.day = 'numeric';
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleDateString('en-US', options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDays = (date: Date, days: number): Date => {
|
||||||
|
const result = new Date(date);
|
||||||
|
result.setDate(result.getDate() + days);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isWithinInterval = (date: Date, interval: { start: Date; end: Date }): boolean => {
|
||||||
|
return date >= interval.start && date <= interval.end;
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: Event | null;
|
event: Event | null;
|
||||||
|
|
@ -262,18 +297,18 @@ const formatEventDate = computed(() => {
|
||||||
|
|
||||||
// Different formats based on timing
|
// Different formats based on timing
|
||||||
if (startDate.toDateString() === now.toDateString()) {
|
if (startDate.toDateString() === now.toDateString()) {
|
||||||
return `Today at ${format(startDate, 'HH:mm')}`;
|
return `Today at ${formatDate(startDate, 'HH:mm')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startDate.toDateString() === addDays(now, 1).toDateString()) {
|
if (startDate.toDateString() === addDays(now, 1).toDateString()) {
|
||||||
return `Tomorrow at ${format(startDate, 'HH:mm')}`;
|
return `Tomorrow at ${formatDate(startDate, 'HH:mm')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startDate.toDateString() === endDate.toDateString()) {
|
if (startDate.toDateString() === endDate.toDateString()) {
|
||||||
return format(startDate, 'EEE, MMM d • HH:mm');
|
return formatDate(startDate, 'EEE, MMM d • HH:mm');
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}`;
|
return `${formatDate(startDate, 'MMM d')} - ${formatDate(endDate, 'MMM d')}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const capacityInfo = computed(() => {
|
const capacityInfo = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
# Deployment Force Update
|
|
||||||
|
|
||||||
This file was created to force a deployment update to include the Events and RSVPs table configuration fields in the admin dialog.
|
|
||||||
|
|
||||||
**Updated**: 2025-08-12 12:49 PM
|
|
||||||
**Reason**: Add missing Events and RSVPs table configuration + Fix API token validation
|
|
||||||
|
|
||||||
## Changes Included:
|
|
||||||
- ✅ Events Table ID configuration field
|
|
||||||
- ✅ RSVPs Table ID configuration field
|
|
||||||
- ✅ Updated AdminConfigurationDialog component (the actual production component)
|
|
||||||
- ✅ Fixed TypeScript errors
|
|
||||||
- ✅ Added proper form validation for new fields
|
|
||||||
- ✅ Fixed ByteString conversion error in API token validation
|
|
||||||
- ✅ Added proper API token validation (no special Unicode characters)
|
|
||||||
|
|
||||||
## Root Cause Identified:
|
|
||||||
1. Production was using AdminConfigurationDialog.vue, not NocoDBSettingsDialog.vue
|
|
||||||
2. API tokens with special characters (bullets, quotes) cause HTTP header errors
|
|
||||||
3. Both issues have now been resolved
|
|
||||||
|
|
@ -1,267 +0,0 @@
|
||||||
# Email Verification Reload Loop - Complete Fix Implementation
|
|
||||||
|
|
||||||
## Problem Analysis
|
|
||||||
|
|
||||||
The email verification page was experiencing endless reload loops on mobile browsers (both Chrome and Safari iOS), caused by:
|
|
||||||
|
|
||||||
1. **Server-Side Token Consumption Bug**: Tokens were consumed immediately on verification, even when Keycloak updates failed
|
|
||||||
2. **Client-Side Navigation Failures**: Mobile browsers failing to navigate away from the verification page
|
|
||||||
3. **Component Lifecycle Issues**: No circuit breaker to prevent repeated API calls
|
|
||||||
4. **Mobile Browser Quirks**: Different timeout and retry behaviors on mobile
|
|
||||||
|
|
||||||
## Root Cause (From System Logs)
|
|
||||||
|
|
||||||
```
|
|
||||||
[verify-email] Keycloak update failed: Failed to update user profile: 400 - {"field":"email","errorMessage":"error-user-attribute-required","params":["email"]}
|
|
||||||
[email-tokens] Token verification failed: Token not found or already used
|
|
||||||
```
|
|
||||||
|
|
||||||
**The flow was**:
|
|
||||||
1. Email verification succeeds, token gets consumed
|
|
||||||
2. Keycloak update fails (configuration issue)
|
|
||||||
3. API returns error, but token is already consumed
|
|
||||||
4. Mobile browser retries same URL
|
|
||||||
5. Token now shows "already used" → endless loop
|
|
||||||
|
|
||||||
## Complete Solution Implementation
|
|
||||||
|
|
||||||
### Phase 1: Server-Side Token Management Fix
|
|
||||||
|
|
||||||
#### A. Enhanced Token Utilities (`server/utils/email-tokens.ts`)
|
|
||||||
|
|
||||||
**Before**: Tokens were consumed immediately during verification
|
|
||||||
**After**: Separated verification from consumption
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// NEW: Verify without consuming
|
|
||||||
export async function verifyEmailToken(token: string): Promise<{ userId: string; email: string }> {
|
|
||||||
// Verify JWT and validate, but DON'T delete token yet
|
|
||||||
return { userId: decoded.userId, email: decoded.email };
|
|
||||||
}
|
|
||||||
|
|
||||||
// NEW: Consume token only after successful operations
|
|
||||||
export async function consumeEmailToken(token: string): Promise<void> {
|
|
||||||
activeTokens.delete(token);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Smart API Endpoint (`server/api/auth/verify-email.get.ts`)
|
|
||||||
|
|
||||||
**Key improvements**:
|
|
||||||
- Only consumes tokens after successful Keycloak updates
|
|
||||||
- Intelligent error classification (retryable vs permanent)
|
|
||||||
- Enhanced response data with partial success indicators
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
// Verify token WITHOUT consuming
|
|
||||||
const { userId, email } = await verifyEmailToken(token);
|
|
||||||
|
|
||||||
// Attempt Keycloak update
|
|
||||||
await keycloak.updateUserProfile(userId, { emailVerified: true });
|
|
||||||
|
|
||||||
// ONLY consume on success
|
|
||||||
await consumeEmailToken(token);
|
|
||||||
|
|
||||||
} catch (keycloakError) {
|
|
||||||
if (keycloakError.message?.includes('error-user-attribute-required')) {
|
|
||||||
// Configuration issue - don't consume token, allow retries
|
|
||||||
partialSuccess = true;
|
|
||||||
} else {
|
|
||||||
// Other errors - consume to prevent infinite loops
|
|
||||||
await consumeEmailToken(token);
|
|
||||||
partialSuccess = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 2: Client-Side Circuit Breaker System
|
|
||||||
|
|
||||||
#### A. Verification State Management (`utils/verification-state.ts`)
|
|
||||||
|
|
||||||
**Features**:
|
|
||||||
- **Browser-persistent state**: Uses sessionStorage with unique keys per token
|
|
||||||
- **Circuit breaker pattern**: Max 3 attempts per 5-minute window
|
|
||||||
- **Progressive navigation**: Multiple fallback methods for mobile compatibility
|
|
||||||
- **Mobile optimizations**: Different delays for Safari iOS vs other browsers
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface VerificationAttempt {
|
|
||||||
token: string;
|
|
||||||
attempts: number;
|
|
||||||
lastAttempt: number;
|
|
||||||
maxAttempts: number;
|
|
||||||
status: 'pending' | 'success' | 'failed' | 'blocked';
|
|
||||||
errors: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progressive navigation with fallbacks
|
|
||||||
export async function navigateWithFallback(url: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
// Method 1: Nuxt navigateTo
|
|
||||||
await navigateTo(url, options);
|
|
||||||
} catch {
|
|
||||||
// Method 2: Vue Router
|
|
||||||
await nuxtApp.$router.replace(url);
|
|
||||||
} catch {
|
|
||||||
// Method 3: Direct window.location (mobile fallback)
|
|
||||||
window.location.replace(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Mobile Browser Optimizations
|
|
||||||
|
|
||||||
**Safari iOS specific**:
|
|
||||||
- 500ms navigation delay for stability
|
|
||||||
- Static device detection to avoid reactive loops
|
|
||||||
- Viewport meta optimization
|
|
||||||
- Hardware acceleration management
|
|
||||||
|
|
||||||
**General mobile**:
|
|
||||||
- 300ms navigation delay
|
|
||||||
- Touch-friendly button sizing
|
|
||||||
- Optimized scroll behavior
|
|
||||||
|
|
||||||
### Phase 3: Enhanced Verification Page
|
|
||||||
|
|
||||||
#### A. Updated UI States (`pages/auth/verify.vue`)
|
|
||||||
|
|
||||||
**New states**:
|
|
||||||
1. **Circuit Breaker Blocked**: Shows when max attempts exceeded
|
|
||||||
2. **Loading with Attempt Counter**: Shows current attempt number
|
|
||||||
3. **Smart Retry Logic**: Only shows retry if attempts remain
|
|
||||||
4. **Comprehensive Error Display**: Different messages for different error types
|
|
||||||
|
|
||||||
#### B. Integration with Circuit Breaker
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Initialize verification state on mount
|
|
||||||
verificationState.value = initVerificationState(token, 3);
|
|
||||||
|
|
||||||
// Check if blocked before attempting
|
|
||||||
if (shouldBlockVerification(token)) {
|
|
||||||
console.log('[auth/verify] Verification blocked by circuit breaker');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record attempts and update UI
|
|
||||||
verificationState.value = recordAttempt(token, success, error);
|
|
||||||
updateUIState();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fix Benefits
|
|
||||||
|
|
||||||
### 🚫 Prevents Reload Loops
|
|
||||||
- **Server**: Tokens preserved for retryable failures
|
|
||||||
- **Client**: Circuit breaker prevents excessive API calls
|
|
||||||
- **Mobile**: Progressive navigation with fallbacks
|
|
||||||
|
|
||||||
### 📱 Mobile Browser Compatibility
|
|
||||||
- **Safari iOS**: Specific delay and navigation optimizations
|
|
||||||
- **Chrome Mobile**: Standard mobile optimizations
|
|
||||||
- **Progressive Fallbacks**: Multiple navigation methods
|
|
||||||
|
|
||||||
### 🔄 Smart Retry Logic
|
|
||||||
- **Automatic Retries**: Up to 3 attempts per 5-minute window
|
|
||||||
- **Intelligent Blocking**: Prevents spam while allowing legitimate retries
|
|
||||||
- **User Feedback**: Clear status messages and attempt counters
|
|
||||||
|
|
||||||
### 🛡️ Error Resilience
|
|
||||||
- **Partial Success Handling**: Works even with Keycloak configuration issues
|
|
||||||
- **Graceful Degradation**: Always provides user feedback and alternatives
|
|
||||||
- **Self-Healing**: Circuit breaker automatically resets after timeout
|
|
||||||
|
|
||||||
## Testing Scenarios Covered
|
|
||||||
|
|
||||||
### ✅ Server Configuration Issues
|
|
||||||
- **Keycloak misconfiguration**: Shows partial success, preserves token
|
|
||||||
- **Database connectivity**: Proper error handling with retry options
|
|
||||||
- **Network timeouts**: Circuit breaker prevents endless attempts
|
|
||||||
|
|
||||||
### ✅ Mobile Browser Edge Cases
|
|
||||||
- **Navigation failures**: Multiple fallback methods
|
|
||||||
- **Component remounting**: Persistent state prevents restart loops
|
|
||||||
- **Memory constraints**: Automatic cleanup of expired states
|
|
||||||
- **Network switching**: Handles connection changes gracefully
|
|
||||||
|
|
||||||
### ✅ User Experience Scenarios
|
|
||||||
- **Expired links**: Clear error messages with alternatives
|
|
||||||
- **Used links**: Proper detection and user guidance
|
|
||||||
- **Multiple tabs**: Each instance has independent circuit breaker
|
|
||||||
- **Back button**: Replace navigation prevents loops
|
|
||||||
|
|
||||||
## Implementation Files
|
|
||||||
|
|
||||||
### Server Files Modified
|
|
||||||
- `server/utils/email-tokens.ts` - Token management overhaul
|
|
||||||
- `server/api/auth/verify-email.get.ts` - Smart verification endpoint
|
|
||||||
|
|
||||||
### Client Files Created/Modified
|
|
||||||
- `utils/verification-state.ts` - Circuit breaker and state management (NEW)
|
|
||||||
- `pages/auth/verify.vue` - Enhanced verification page with circuit breaker
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- Existing static device detection (`utils/static-device-detection.ts`)
|
|
||||||
- Existing mobile Safari optimizations (`utils/mobile-safari-utils.ts`)
|
|
||||||
|
|
||||||
## Monitoring and Debugging
|
|
||||||
|
|
||||||
### Server-Side Logging
|
|
||||||
```
|
|
||||||
[email-tokens] Token consumed successfully
|
|
||||||
[verify-email] Keycloak configuration error - token preserved for retry
|
|
||||||
[verify-email] Consuming token despite Keycloak error to prevent loops
|
|
||||||
```
|
|
||||||
|
|
||||||
### Client-Side Logging
|
|
||||||
```
|
|
||||||
[verification-state] Maximum attempts (3) reached, blocking further attempts
|
|
||||||
[verification-state] Verification blocked for 8 more minutes
|
|
||||||
[verification-state] Using window.location fallback
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Circuit Breaker Settings
|
|
||||||
```typescript
|
|
||||||
const MAX_ATTEMPTS_DEFAULT = 3;
|
|
||||||
const ATTEMPT_WINDOW = 5 * 60 * 1000; // 5 minutes
|
|
||||||
const CIRCUIT_BREAKER_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mobile Navigation Delays
|
|
||||||
```typescript
|
|
||||||
// Safari iOS: 500ms delay
|
|
||||||
// Other mobile: 300ms delay
|
|
||||||
// Desktop: 100ms delay
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Notes
|
|
||||||
|
|
||||||
### Immediate Benefits
|
|
||||||
- Existing verification links will work better
|
|
||||||
- No database migrations required
|
|
||||||
- Backward compatible with existing tokens
|
|
||||||
|
|
||||||
### Long-term Improvements
|
|
||||||
- Reduced server load from repeated failed attempts
|
|
||||||
- Better user experience with clear status messages
|
|
||||||
- Automatic recovery from temporary configuration issues
|
|
||||||
|
|
||||||
## Success Metrics
|
|
||||||
|
|
||||||
### Before Fix
|
|
||||||
- Endless reload loops on mobile browsers
|
|
||||||
- Token consumption on partial failures
|
|
||||||
- No retry mechanism for temporary issues
|
|
||||||
- Poor mobile browser navigation compatibility
|
|
||||||
|
|
||||||
### After Fix
|
|
||||||
- ✅ Circuit breaker prevents reload loops
|
|
||||||
- ✅ Smart token consumption based on actual success
|
|
||||||
- ✅ Intelligent retry with user feedback
|
|
||||||
- ✅ Progressive navigation with mobile fallbacks
|
|
||||||
- ✅ Comprehensive error handling and user guidance
|
|
||||||
|
|
||||||
This fix addresses the root cause while providing comprehensive resilience for all edge cases and browser combinations.
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
# Events System - Comprehensive Bug Analysis
|
|
||||||
|
|
||||||
## CRITICAL BUGS IDENTIFIED:
|
|
||||||
|
|
||||||
### 1. **MAJOR: Database Architecture Flaw**
|
|
||||||
**File:** `server/utils/nocodb-events.ts`
|
|
||||||
**Issue:** The system attempts to use the same table for both Events and RSVPs, causing data corruption
|
|
||||||
**Severity:** CRITICAL - System Breaking
|
|
||||||
**Status:** PARTIALLY FIXED - Still has configuration issues
|
|
||||||
|
|
||||||
### 2. **CRITICAL: Configuration Missing**
|
|
||||||
**File:** `nuxt.config.ts`
|
|
||||||
**Issue:** Missing events-specific NocoDB configuration properties
|
|
||||||
**Impact:** Events system cannot initialize properly
|
|
||||||
**Missing Properties:**
|
|
||||||
- `eventsBaseId`
|
|
||||||
- `eventsTableId`
|
|
||||||
- `rsvpTableId`
|
|
||||||
|
|
||||||
### 3. **MAJOR: RSVP Functions Wrong Table**
|
|
||||||
**File:** `server/utils/nocodb-events.ts`
|
|
||||||
**Issue:** All RSVP functions still point to events table instead of RSVP table
|
|
||||||
**Impact:** RSVPs stored in wrong table, data corruption
|
|
||||||
|
|
||||||
### 4. **CRITICAL: Type Safety Issues**
|
|
||||||
**File:** `server/utils/nocodb-events.ts`
|
|
||||||
**Issue:** Multiple `unknown` types causing runtime errors
|
|
||||||
**Impact:** Calendar fails to load, RSVP system breaks
|
|
||||||
|
|
||||||
### 5. **MAJOR: API Endpoint Issues**
|
|
||||||
**Files:** All `server/api/events/` files
|
|
||||||
**Issue:** Recently fixed authentication but still has logical bugs
|
|
||||||
**Remaining Issues:**
|
|
||||||
- No validation of event data
|
|
||||||
- Missing error handling for database failures
|
|
||||||
- Inconsistent response formats
|
|
||||||
|
|
||||||
### 6. **CRITICAL: Frontend Component Bugs**
|
|
||||||
**File:** `components/CreateEventDialog.vue`
|
|
||||||
**Issues:**
|
|
||||||
- Form validation insufficient
|
|
||||||
- Missing error handling for API failures
|
|
||||||
- Date/time formatting issues
|
|
||||||
- No loading states for better UX
|
|
||||||
|
|
||||||
### 7. **MAJOR: Calendar Component Issues**
|
|
||||||
**File:** `components/EventCalendar.vue`
|
|
||||||
**Issues:**
|
|
||||||
- Event transformation logic flawed
|
|
||||||
- Mobile view switching problems
|
|
||||||
- FullCalendar integration missing key features
|
|
||||||
- No error boundaries for calendar failures
|
|
||||||
|
|
||||||
### 8. **CRITICAL: Event Details Dialog Bugs**
|
|
||||||
**File:** `components/EventDetailsDialog.vue`
|
|
||||||
**Issues:**
|
|
||||||
- RSVP submission hardcoded member_id as empty string
|
|
||||||
- Payment info hardcoded instead of from config
|
|
||||||
- Missing proper error handling
|
|
||||||
- No loading states
|
|
||||||
|
|
||||||
### 9. **MAJOR: UseEvents Composable Issues**
|
|
||||||
**File:** `composables/useEvents.ts`
|
|
||||||
**Issues:**
|
|
||||||
- Calendar events function not properly integrated
|
|
||||||
- Cache key generation problematic
|
|
||||||
- Error propagation inconsistent
|
|
||||||
- Date handling utilities missing
|
|
||||||
|
|
||||||
### 10. **CRITICAL: Environment Configuration Incomplete**
|
|
||||||
**File:** `nuxt.config.ts` and `.env.example`
|
|
||||||
**Issues:**
|
|
||||||
- Missing events-specific environment variables
|
|
||||||
- No fallback values for development
|
|
||||||
- Events base/table IDs not configured
|
|
||||||
|
|
||||||
## ARCHITECTURAL PROBLEMS:
|
|
||||||
|
|
||||||
### 1. **Data Model Confusion**
|
|
||||||
The system tries to store Events and RSVPs in the same table, which is fundamentally wrong:
|
|
||||||
- Events need their own table with event-specific fields
|
|
||||||
- RSVPs need a separate table with foreign key to events
|
|
||||||
- Current mixing causes data corruption and query failures
|
|
||||||
|
|
||||||
### 2. **Configuration Inconsistency**
|
|
||||||
Events system references configuration properties that don't exist:
|
|
||||||
- `config.nocodb.eventsBaseId` - doesn't exist
|
|
||||||
- `config.nocodb.eventsTableId` - doesn't exist
|
|
||||||
- `config.nocodb.rsvpTableId` - doesn't exist
|
|
||||||
|
|
||||||
### 3. **API Response Inconsistency**
|
|
||||||
Different endpoints return different response formats:
|
|
||||||
- Some return `{ success, data, message }`
|
|
||||||
- Others return raw NocoDB responses
|
|
||||||
- Frontend expects consistent format
|
|
||||||
|
|
||||||
### 4. **Frontend State Management Issues**
|
|
||||||
- No centralized error handling
|
|
||||||
- Inconsistent loading states
|
|
||||||
- Cache invalidation problems
|
|
||||||
- Component state synchronization issues
|
|
||||||
|
|
||||||
## IMMEDIATE FIXES REQUIRED:
|
|
||||||
|
|
||||||
### Phase 1 - Critical Infrastructure
|
|
||||||
1. Fix NocoDB configuration in `nuxt.config.ts`
|
|
||||||
2. Separate Events and RSVPs into different tables/functions
|
|
||||||
3. Fix all TypeScript errors
|
|
||||||
4. Ensure basic API endpoints work
|
|
||||||
|
|
||||||
### Phase 2 - API Stability
|
|
||||||
1. Standardize API response formats
|
|
||||||
2. Add proper validation and error handling
|
|
||||||
3. Fix authentication integration
|
|
||||||
4. Test all CRUD operations
|
|
||||||
|
|
||||||
### Phase 3 - Frontend Polish
|
|
||||||
1. Fix component error handling
|
|
||||||
2. Add proper loading states
|
|
||||||
3. Fix form validation
|
|
||||||
4. Test calendar integration
|
|
||||||
|
|
||||||
### Phase 4 - Integration Testing
|
|
||||||
1. End-to-end event creation flow
|
|
||||||
2. RSVP submission and management
|
|
||||||
3. Calendar display and interaction
|
|
||||||
4. Mobile responsiveness
|
|
||||||
|
|
||||||
## RECOMMENDED APPROACH:
|
|
||||||
|
|
||||||
1. **Stop using current events system** - it will cause data corruption
|
|
||||||
2. **Fix configuration first** - add missing environment variables
|
|
||||||
3. **Separate data models** - create proper Events and RSVPs tables
|
|
||||||
4. **Rebuild API layer** - ensure consistency and reliability
|
|
||||||
5. **Fix frontend components** - proper error handling and state management
|
|
||||||
6. **Full integration testing** - ensure entire flow works end-to-end
|
|
||||||
|
|
||||||
## ESTIMATED EFFORT:
|
|
||||||
- **Critical fixes:** 4-6 hours
|
|
||||||
- **Full system stability:** 8-12 hours
|
|
||||||
- **Polish and testing:** 4-6 hours
|
|
||||||
- **Total:** 16-24 hours of focused development time
|
|
||||||
|
|
||||||
## RISK ASSESSMENT:
|
|
||||||
- **Current system:** HIGH RISK - will cause data loss/corruption
|
|
||||||
- **After Phase 1 fixes:** MEDIUM RISK - basic functionality restored
|
|
||||||
- **After Phase 2 fixes:** LOW RISK - production ready
|
|
||||||
- **After Phase 3-4:** MINIMAL RISK - polished and tested
|
|
||||||
|
|
@ -1,280 +0,0 @@
|
||||||
# MonacoUSA Portal - Integration Review & Troubleshooting Guide
|
|
||||||
|
|
||||||
## SMTP Email Integration Points
|
|
||||||
|
|
||||||
### 1. Email Configuration Storage
|
|
||||||
- **Location**: `server/utils/admin-config.ts`
|
|
||||||
- **Storage**: Encrypted in `/app/data/admin-config.json` (Docker) or `./data/admin-config.json` (local)
|
|
||||||
- **Fields**: host, port, secure, username, password, fromAddress, fromName
|
|
||||||
|
|
||||||
### 2. Email Service Implementation
|
|
||||||
- **Location**: `server/utils/email.ts`
|
|
||||||
- **Features**:
|
|
||||||
- Auto-detects security settings based on port
|
|
||||||
- Increased timeouts (60 seconds) for slow servers
|
|
||||||
- Supports STARTTLS (port 587) and SSL/TLS (port 465)
|
|
||||||
- Authentication type set to 'login' for compatibility
|
|
||||||
- Accepts self-signed certificates
|
|
||||||
|
|
||||||
### 3. Email Usage Points
|
|
||||||
- **Registration**: `server/api/registration.post.ts` - Sends welcome email with verification link
|
|
||||||
- **Portal Account Creation**: `server/api/members/[id]/create-portal-account.post.ts` - Sends welcome email
|
|
||||||
- **Password Reset**: `server/api/auth/forgot-password.post.ts` - Sends password reset link
|
|
||||||
- **Email Verification Resend**: `server/api/auth/send-verification-email.post.ts`
|
|
||||||
- **Test Email**: `server/api/admin/test-email.post.ts` - Admin panel test
|
|
||||||
|
|
||||||
### 4. Common SMTP Issues & Solutions
|
|
||||||
|
|
||||||
#### Issue: "500 plugin timeout" / EAUTH errors
|
|
||||||
**Solutions**:
|
|
||||||
1. **Port 587 (STARTTLS)**:
|
|
||||||
- Set SSL/TLS: OFF
|
|
||||||
- Username: Full email address (noreply@monacousa.org)
|
|
||||||
- Password: Your SMTP password (not email password if different)
|
|
||||||
|
|
||||||
2. **Port 465 (SSL/TLS)**:
|
|
||||||
- Set SSL/TLS: ON
|
|
||||||
- Same credentials as above
|
|
||||||
|
|
||||||
3. **Port 25 (Unencrypted)**:
|
|
||||||
- Set SSL/TLS: OFF
|
|
||||||
- May not require authentication
|
|
||||||
- Not recommended for production
|
|
||||||
|
|
||||||
4. **Alternative Configuration** for mail.monacousa.org:
|
|
||||||
- Try port 587 with SSL/TLS OFF
|
|
||||||
- Try port 465 with SSL/TLS ON
|
|
||||||
- Ensure username is full email address
|
|
||||||
- Some servers require app-specific passwords
|
|
||||||
|
|
||||||
#### Issue: Connection timeouts
|
|
||||||
**Solutions**:
|
|
||||||
- Timeouts already increased to 60 seconds
|
|
||||||
- Check firewall rules allow outbound connections on SMTP port
|
|
||||||
- Verify DNS resolution of mail server
|
|
||||||
|
|
||||||
#### Issue: Certificate errors
|
|
||||||
**Solutions**:
|
|
||||||
- Self-signed certificates are already accepted
|
|
||||||
- TLS minimum version set to TLSv1 for compatibility
|
|
||||||
|
|
||||||
### 5. Testing SMTP Without Email
|
|
||||||
If SMTP cannot be configured, the system gracefully handles email failures:
|
|
||||||
- Portal accounts are still created
|
|
||||||
- Users can use "Forgot Password" to set initial password
|
|
||||||
- Admin sees appropriate messages about email status
|
|
||||||
|
|
||||||
## Keycloak Integration Points
|
|
||||||
|
|
||||||
### 1. Authentication Flow
|
|
||||||
- **Login**: `server/api/auth/keycloak/login.get.ts` - Redirects to Keycloak
|
|
||||||
- **Callback**: `server/api/auth/keycloak/callback.get.ts` - Handles OAuth callback
|
|
||||||
- **Session**: `server/utils/session.ts` - Manages encrypted sessions
|
|
||||||
- **Logout**: `server/api/auth/logout.post.ts` - Clears session and Keycloak logout
|
|
||||||
|
|
||||||
### 2. User Management
|
|
||||||
- **Admin Client**: `server/utils/keycloak-admin.ts`
|
|
||||||
- **Features**:
|
|
||||||
- Create users with role-based registration
|
|
||||||
- Update user attributes (membership data)
|
|
||||||
- Password reset functionality
|
|
||||||
- Email verification tokens
|
|
||||||
- User search by email
|
|
||||||
|
|
||||||
### 3. Role-Based Access
|
|
||||||
- **Tiers**: admin, board, user
|
|
||||||
- **Middleware**:
|
|
||||||
- `middleware/auth.ts` - General authentication
|
|
||||||
- `middleware/auth-admin.ts` - Admin only
|
|
||||||
- `middleware/auth-board.ts` - Board and admin
|
|
||||||
- `middleware/auth-user.ts` - All authenticated users
|
|
||||||
|
|
||||||
### 4. Member-Portal Sync
|
|
||||||
- **Dual Database System**:
|
|
||||||
- NocoDB: Member records (source of truth)
|
|
||||||
- Keycloak: Authentication and portal accounts
|
|
||||||
- **Sync Points**:
|
|
||||||
- Registration creates both records
|
|
||||||
- Portal account creation links existing member to Keycloak
|
|
||||||
- Member updates sync to Keycloak attributes
|
|
||||||
|
|
||||||
### 5. Common Keycloak Issues & Solutions
|
|
||||||
|
|
||||||
#### Issue: Login redirect loops
|
|
||||||
**Solutions**:
|
|
||||||
- Check `NUXT_KEYCLOAK_CALLBACK_URL` matches actual domain
|
|
||||||
- Verify Keycloak client redirect URIs include callback URL
|
|
||||||
- Ensure session secret is set and consistent
|
|
||||||
|
|
||||||
#### Issue: User creation failures
|
|
||||||
**Solutions**:
|
|
||||||
- Check Keycloak admin credentials in environment
|
|
||||||
- Verify realm exists and is accessible
|
|
||||||
- Ensure email is unique in Keycloak
|
|
||||||
|
|
||||||
#### Issue: Role assignment not working
|
|
||||||
**Solutions**:
|
|
||||||
- Verify realm roles exist: user, board, admin
|
|
||||||
- Check client scope mappings include roles
|
|
||||||
- Ensure token includes role claims
|
|
||||||
|
|
||||||
## Environment Variables Required
|
|
||||||
|
|
||||||
### Keycloak Configuration
|
|
||||||
```env
|
|
||||||
NUXT_KEYCLOAK_ISSUER=https://auth.monacousa.org/realms/monacousa-portal
|
|
||||||
NUXT_KEYCLOAK_CLIENT_ID=monacousa-portal
|
|
||||||
NUXT_KEYCLOAK_CLIENT_SECRET=your-client-secret
|
|
||||||
NUXT_KEYCLOAK_CALLBACK_URL=https://monacousa.org/auth/callback
|
|
||||||
NUXT_KEYCLOAK_ADMIN_USERNAME=admin
|
|
||||||
NUXT_KEYCLOAK_ADMIN_PASSWORD=admin-password
|
|
||||||
```
|
|
||||||
|
|
||||||
### Session Security
|
|
||||||
```env
|
|
||||||
NUXT_SESSION_SECRET=48-character-secret-key
|
|
||||||
NUXT_ENCRYPTION_KEY=32-character-encryption-key
|
|
||||||
```
|
|
||||||
|
|
||||||
### Public Configuration
|
|
||||||
```env
|
|
||||||
NUXT_PUBLIC_DOMAIN=monacousa.org
|
|
||||||
```
|
|
||||||
|
|
||||||
## Health Check Endpoints
|
|
||||||
|
|
||||||
### System Health
|
|
||||||
- **Endpoint**: `GET /api/health`
|
|
||||||
- **Checks**:
|
|
||||||
- Database connectivity (NocoDB)
|
|
||||||
- Keycloak connectivity
|
|
||||||
- Session management
|
|
||||||
- File storage (if configured)
|
|
||||||
|
|
||||||
## Troubleshooting Workflow
|
|
||||||
|
|
||||||
### For SMTP Issues:
|
|
||||||
1. Try port 587 with SSL/TLS OFF first
|
|
||||||
2. If fails, try port 465 with SSL/TLS ON
|
|
||||||
3. Check credentials (use full email as username)
|
|
||||||
4. Test with personal Gmail/Outlook account to verify code works
|
|
||||||
5. Check firewall/network restrictions
|
|
||||||
6. Review server logs for specific error messages
|
|
||||||
|
|
||||||
### For Keycloak Issues:
|
|
||||||
1. Verify all environment variables are set
|
|
||||||
2. Check Keycloak server is accessible
|
|
||||||
3. Test with direct Keycloak login first
|
|
||||||
4. Review browser console for redirect issues
|
|
||||||
5. Check server logs for token/session errors
|
|
||||||
6. Verify realm and client configuration in Keycloak admin
|
|
||||||
|
|
||||||
## Manual SMTP Testing
|
|
||||||
|
|
||||||
To manually test SMTP settings without the portal:
|
|
||||||
|
|
||||||
### Using OpenSSL (for connection test):
|
|
||||||
```bash
|
|
||||||
# For STARTTLS (port 587)
|
|
||||||
openssl s_client -starttls smtp -connect mail.monacousa.org:587
|
|
||||||
|
|
||||||
# For SSL/TLS (port 465)
|
|
||||||
openssl s_client -connect mail.monacousa.org:465
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Telnet (for basic connectivity):
|
|
||||||
```bash
|
|
||||||
telnet mail.monacousa.org 587
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using swaks (comprehensive SMTP test):
|
|
||||||
```bash
|
|
||||||
swaks --to test@example.com \
|
|
||||||
--from noreply@monacousa.org \
|
|
||||||
--server mail.monacousa.org:587 \
|
|
||||||
--auth LOGIN \
|
|
||||||
--auth-user noreply@monacousa.org \
|
|
||||||
--auth-password yourpassword \
|
|
||||||
--tls
|
|
||||||
```
|
|
||||||
|
|
||||||
## Alternative Email Solutions
|
|
||||||
|
|
||||||
If SMTP continues to fail:
|
|
||||||
|
|
||||||
### 1. Use Gmail with App Password:
|
|
||||||
- Enable 2FA on Gmail account
|
|
||||||
- Generate app-specific password
|
|
||||||
- Use smtp.gmail.com:587
|
|
||||||
- Username: your gmail address
|
|
||||||
- Password: app-specific password
|
|
||||||
|
|
||||||
### 2. Use SendGrid (Free tier available):
|
|
||||||
- Sign up at sendgrid.com
|
|
||||||
- Create API key
|
|
||||||
- Use smtp.sendgrid.net:587
|
|
||||||
- Username: apikey (literal string)
|
|
||||||
- Password: your API key
|
|
||||||
|
|
||||||
### 3. Use Local Mail Server (Development):
|
|
||||||
- Install MailHog or MailCatcher
|
|
||||||
- No authentication required
|
|
||||||
- Captures all emails locally
|
|
||||||
- Perfect for testing
|
|
||||||
|
|
||||||
## System Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐ ┌──────────────┐ ┌─────────────┐
|
|
||||||
│ │────▶│ │────▶│ │
|
|
||||||
│ Frontend │ │ Nuxt API │ │ Keycloak │
|
|
||||||
│ (Vue/Vuetify) │◀────│ Routes │◀────│ Server │
|
|
||||||
│ │ │ │ │ │
|
|
||||||
└─────────────────┘ └──────────────┘ └─────────────┘
|
|
||||||
│ ▲
|
|
||||||
│ │
|
|
||||||
▼ │
|
|
||||||
┌──────────────┐ │
|
|
||||||
│ │ │
|
|
||||||
│ NocoDB │─────────────┘
|
|
||||||
│ Database │
|
|
||||||
│ │
|
|
||||||
└──────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌──────────────┐
|
|
||||||
│ │
|
|
||||||
│ SMTP │
|
|
||||||
│ Server │
|
|
||||||
│ │
|
|
||||||
└──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production Checklist
|
|
||||||
|
|
||||||
- [ ] All environment variables set correctly
|
|
||||||
- [ ] SSL certificates valid and configured
|
|
||||||
- [ ] Keycloak realm and client configured
|
|
||||||
- [ ] NocoDB database accessible and configured
|
|
||||||
- [ ] SMTP credentials tested and working
|
|
||||||
- [ ] Session secrets are strong and unique
|
|
||||||
- [ ] Firewall rules allow necessary ports
|
|
||||||
- [ ] Backup strategy in place
|
|
||||||
- [ ] Monitoring and logging configured
|
|
||||||
- [ ] Health check endpoint monitored
|
|
||||||
|
|
||||||
## Support Resources
|
|
||||||
|
|
||||||
- **Keycloak Documentation**: https://www.keycloak.org/documentation
|
|
||||||
- **NocoDB Documentation**: https://docs.nocodb.com
|
|
||||||
- **Nodemailer Documentation**: https://nodemailer.com
|
|
||||||
- **Nuxt 3 Documentation**: https://nuxt.com
|
|
||||||
|
|
||||||
## Contact for Issues
|
|
||||||
|
|
||||||
If you continue to experience issues after following this guide:
|
|
||||||
1. Check server logs for detailed error messages
|
|
||||||
2. Test each component independently
|
|
||||||
3. Verify network connectivity and DNS resolution
|
|
||||||
4. Review firewall and security group rules
|
|
||||||
5. Consider using alternative email providers
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
# Mobile Browser Reload Loop - Complete Fix
|
|
||||||
|
|
||||||
## Problem Summary
|
|
||||||
|
|
||||||
After fixing the initial email verification reload loop, the issue propagated to other auth pages:
|
|
||||||
- **Email verification success page** constantly reloaded on mobile
|
|
||||||
- **Password setup page** constantly reloaded on mobile
|
|
||||||
- **Verification expired page** had similar issues
|
|
||||||
|
|
||||||
## Root Cause Analysis
|
|
||||||
|
|
||||||
The problem was **reactive computed properties** that watched `route.query` parameters:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// PROBLEMATIC - causes reload loops on mobile
|
|
||||||
const email = computed(() => route.query.email as string || '');
|
|
||||||
const partialWarning = computed(() => route.query.warning === 'partial');
|
|
||||||
const token = computed(() => route.query.token as string || '');
|
|
||||||
const reason = computed(() => route.query.reason as string || 'expired');
|
|
||||||
```
|
|
||||||
|
|
||||||
In mobile browsers (especially Safari iOS), these reactive computeds can trigger infinite update loops:
|
|
||||||
1. Page loads with route.query values
|
|
||||||
2. Computed properties watch these values reactively
|
|
||||||
3. Mobile browser reactivity can trigger spurious updates
|
|
||||||
4. Page reloads, cycle continues
|
|
||||||
|
|
||||||
## Complete Solution Implemented
|
|
||||||
|
|
||||||
### ✅ Fixed All Affected Pages
|
|
||||||
|
|
||||||
**1. pages/auth/verify-success.vue**
|
|
||||||
```typescript
|
|
||||||
// BEFORE (reactive - causes loops)
|
|
||||||
const email = computed(() => route.query.email as string || '');
|
|
||||||
const partialWarning = computed(() => route.query.warning === 'partial');
|
|
||||||
|
|
||||||
// AFTER (static - no loops)
|
|
||||||
const email = ref((route.query.email as string) || '');
|
|
||||||
const partialWarning = ref(route.query.warning === 'partial');
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. pages/auth/setup-password.vue**
|
|
||||||
```typescript
|
|
||||||
// BEFORE (reactive - causes loops)
|
|
||||||
const email = computed(() => route.query.email as string || '');
|
|
||||||
const token = computed(() => route.query.token as string || '');
|
|
||||||
|
|
||||||
// AFTER (static - no loops)
|
|
||||||
const email = ref((route.query.email as string) || '');
|
|
||||||
const token = ref((route.query.token as string) || '');
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. pages/auth/verify-expired.vue**
|
|
||||||
```typescript
|
|
||||||
// BEFORE (reactive - causes loops)
|
|
||||||
const reason = computed(() => route.query.reason as string || 'expired');
|
|
||||||
|
|
||||||
// AFTER (static - no loops)
|
|
||||||
const reason = ref((route.query.reason as string) || 'expired');
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. pages/auth/verify.vue**
|
|
||||||
- ✅ Already fixed with comprehensive circuit breaker system
|
|
||||||
- ✅ Uses static device detection and verification state management
|
|
||||||
|
|
||||||
## Key Principle
|
|
||||||
|
|
||||||
**Static Query Parameter Capture**: Instead of reactively watching route query parameters, capture them once on page load as static refs. This prevents mobile browser reactivity loops while maintaining functionality.
|
|
||||||
|
|
||||||
## Testing Verified
|
|
||||||
|
|
||||||
### ✅ Mobile Safari iOS
|
|
||||||
- Email verification flow works end-to-end
|
|
||||||
- Success page loads without reload loops
|
|
||||||
- Password setup page works properly
|
|
||||||
- All navigation functions correctly
|
|
||||||
|
|
||||||
### ✅ Chrome Mobile Android
|
|
||||||
- All auth pages load without reload loops
|
|
||||||
- Progressive navigation fallbacks work
|
|
||||||
- Form submissions and redirects function properly
|
|
||||||
|
|
||||||
### ✅ Desktop Browsers
|
|
||||||
- All existing functionality preserved
|
|
||||||
- No performance regressions
|
|
||||||
- Enhanced error handling maintained
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
**Auth Pages Fixed:**
|
|
||||||
- `pages/auth/verify-success.vue` - Static email and warning refs
|
|
||||||
- `pages/auth/setup-password.vue` - Static email and token refs
|
|
||||||
- `pages/auth/verify-expired.vue` - Static reason ref
|
|
||||||
- `pages/auth/verify.vue` - Already had circuit breaker (no changes needed)
|
|
||||||
|
|
||||||
**Supporting Infrastructure:**
|
|
||||||
- `server/utils/email-tokens.ts` - Smart token consumption
|
|
||||||
- `server/api/auth/verify-email.get.ts` - Enhanced error handling
|
|
||||||
- `utils/verification-state.ts` - Circuit breaker system
|
|
||||||
- All mobile Safari optimizations maintained
|
|
||||||
|
|
||||||
## Mobile Browser Compatibility
|
|
||||||
|
|
||||||
### Safari iOS
|
|
||||||
✅ No reload loops on any auth pages
|
|
||||||
✅ Proper navigation between pages
|
|
||||||
✅ Form submissions work correctly
|
|
||||||
✅ PWA functionality maintained
|
|
||||||
|
|
||||||
### Chrome Mobile
|
|
||||||
✅ All auth flows work properly
|
|
||||||
✅ No performance issues
|
|
||||||
✅ Touch targets optimized
|
|
||||||
✅ Viewport handling correct
|
|
||||||
|
|
||||||
### Edge Mobile & Others
|
|
||||||
✅ Progressive fallbacks ensure compatibility
|
|
||||||
✅ Static query handling works universally
|
|
||||||
✅ No browser-specific issues
|
|
||||||
|
|
||||||
## Deployment Ready
|
|
||||||
|
|
||||||
- **Zero Breaking Changes**: All existing functionality preserved
|
|
||||||
- **Backward Compatible**: Existing links and bookmarks still work
|
|
||||||
- **Performance Optimized**: Reduced reactive overhead on mobile
|
|
||||||
- **Comprehensive Testing**: All auth flows verified on multiple devices
|
|
||||||
|
|
||||||
## Success Metrics
|
|
||||||
|
|
||||||
### Before Fix
|
|
||||||
❌ Email verification success page: endless reload loops
|
|
||||||
❌ Password setup page: endless reload loops
|
|
||||||
❌ Mobile Safari: unusable auth experience
|
|
||||||
❌ High server load from repeated requests
|
|
||||||
|
|
||||||
### After Fix
|
|
||||||
✅ All auth pages load successfully on mobile
|
|
||||||
✅ Complete end-to-end verification flow works
|
|
||||||
✅ Zero reload loops on any mobile browser
|
|
||||||
✅ Reduced server load with circuit breaker
|
|
||||||
✅ Enhanced user experience with clear error states
|
|
||||||
|
|
||||||
**Result**: The MonacoUSA Portal email verification and password setup flow now works flawlessly across all mobile browsers, providing a smooth user experience for account registration and verification.
|
|
||||||
|
|
@ -1,344 +0,0 @@
|
||||||
# Mobile Safari Reload Loop Prevention - Comprehensive Solution
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document describes the comprehensive reload loop prevention system implemented to resolve infinite reload loops on mobile Safari for the signup, email verification, and password setup pages. This solution builds upon previous fixes with advanced detection, prevention, and recovery mechanisms.
|
|
||||||
|
|
||||||
## Problem Analysis
|
|
||||||
|
|
||||||
### Root Causes Identified
|
|
||||||
|
|
||||||
1. **Reactive Dependency Loops**: Vue's reactivity system creating cascading re-renders
|
|
||||||
2. **Config Cache Corruption**: Race conditions in configuration loading
|
|
||||||
3. **Mobile Safari Specific Issues**:
|
|
||||||
- Aggressive back/forward cache (bfcache)
|
|
||||||
- Viewport handling inconsistencies
|
|
||||||
- Navigation timing issues
|
|
||||||
4. **API Call Cascades**: Repeated config API calls triggering reload cycles
|
|
||||||
5. **Error Propagation**: Unhandled errors causing page reloads
|
|
||||||
|
|
||||||
## Solution Architecture
|
|
||||||
|
|
||||||
### 1. Advanced Reload Loop Detection (`utils/reload-loop-prevention.ts`)
|
|
||||||
|
|
||||||
**Core Features:**
|
|
||||||
- **Page Load Tracking**: Monitors page load frequency per URL
|
|
||||||
- **Circuit Breaker Pattern**: Automatically blocks pages after 5 loads in 10 seconds
|
|
||||||
- **Emergency Mode**: 30-second block with user-friendly message
|
|
||||||
- **Mobile Safari Integration**: Specific handling for Safari's bfcache and navigation quirks
|
|
||||||
|
|
||||||
**Key Functions:**
|
|
||||||
```typescript
|
|
||||||
// Initialize protection for a page
|
|
||||||
const canLoad = initReloadLoopPrevention('page-name');
|
|
||||||
if (!canLoad) {
|
|
||||||
return; // Page blocked, show emergency message
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a specific page is blocked
|
|
||||||
const isBlocked = isPageBlocked('/auth/verify');
|
|
||||||
|
|
||||||
// Get current status for debugging
|
|
||||||
const status = getReloadLoopStatus();
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Enhanced Config Cache Plugin (`plugins/04.config-cache-init.client.ts`)
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Reload Loop Integration**: Checks prevention system before initialization
|
|
||||||
- **Advanced Error Handling**: Catches more error patterns
|
|
||||||
- **API Call Monitoring**: Detects excessive API calls (>10 in 5 seconds)
|
|
||||||
- **Performance Monitoring**: Tracks page reload events
|
|
||||||
- **Visibility Change Handling**: Manages cache integrity when page visibility changes
|
|
||||||
|
|
||||||
**Enhanced Protection:**
|
|
||||||
```typescript
|
|
||||||
// Comprehensive error patterns
|
|
||||||
const isReloadLoop = (
|
|
||||||
msg.includes('Maximum call stack') ||
|
|
||||||
msg.includes('too much recursion') ||
|
|
||||||
msg.includes('RangeError') ||
|
|
||||||
msg.includes('Script error') ||
|
|
||||||
msg.includes('ResizeObserver loop limit exceeded') ||
|
|
||||||
msg.includes('Non-Error promise rejection captured')
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Page-Level Integration
|
|
||||||
|
|
||||||
**Signup Page (`pages/signup.vue`):**
|
|
||||||
- Reload loop check before all initialization
|
|
||||||
- Timeout protection for config loading (10 seconds)
|
|
||||||
- Enhanced error handling with cache cleanup
|
|
||||||
- Graceful degradation to default values
|
|
||||||
|
|
||||||
**Verification Page (`pages/auth/verify.vue`):**
|
|
||||||
- Early reload loop prevention check
|
|
||||||
- Integration with existing circuit breaker
|
|
||||||
- Protected navigation with mobile delays
|
|
||||||
|
|
||||||
**Password Setup Page (`pages/auth/setup-password.vue`):**
|
|
||||||
- Immediate reload loop prevention
|
|
||||||
- Protected initialization sequence
|
|
||||||
|
|
||||||
## Key Improvements
|
|
||||||
|
|
||||||
### 1. Early Detection System
|
|
||||||
```typescript
|
|
||||||
// Check BEFORE any initialization
|
|
||||||
const { initReloadLoopPrevention } = await import('~/utils/reload-loop-prevention');
|
|
||||||
const canLoad = initReloadLoopPrevention('page-name');
|
|
||||||
|
|
||||||
if (!canLoad) {
|
|
||||||
console.error('Page load blocked by reload loop prevention system');
|
|
||||||
return; // Stop all initialization
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Mobile Safari Optimizations
|
|
||||||
```typescript
|
|
||||||
// Auto-applied mobile Safari fixes
|
|
||||||
applyMobileSafariReloadLoopFixes();
|
|
||||||
|
|
||||||
// Handles bfcache restoration
|
|
||||||
window.addEventListener('pageshow', (event) => {
|
|
||||||
if (event.persisted) {
|
|
||||||
// Handle back/forward cache restoration
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Enhanced API Monitoring
|
|
||||||
```typescript
|
|
||||||
// Monitor fetch calls for loops
|
|
||||||
window.fetch = function(input, init) {
|
|
||||||
// Track API call frequency
|
|
||||||
// Block excessive config API calls
|
|
||||||
// Log suspicious patterns
|
|
||||||
return originalFetch.call(this, input, init);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Emergency User Interface
|
|
||||||
When a reload loop is detected, users see:
|
|
||||||
- Clear explanation of the issue
|
|
||||||
- Estimated time until block is lifted (30 seconds)
|
|
||||||
- Alternative navigation options (Home, Back)
|
|
||||||
- Contact information for support
|
|
||||||
|
|
||||||
## Testing Instructions
|
|
||||||
|
|
||||||
### Manual Testing on Mobile Safari
|
|
||||||
|
|
||||||
1. **Basic Load Test:**
|
|
||||||
```bash
|
|
||||||
# Navigate to each page multiple times rapidly
|
|
||||||
/signup
|
|
||||||
/auth/verify?token=test
|
|
||||||
/auth/setup-password?email=test@test.com
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Reload Loop Simulation:**
|
|
||||||
```javascript
|
|
||||||
// In browser console, simulate rapid reloads
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Config API Testing:**
|
|
||||||
```javascript
|
|
||||||
// Test circuit breaker
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
fetch('/api/recaptcha-config');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Automated Testing Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test page load times
|
|
||||||
curl -w "%{time_total}" https://monacousa.org/signup
|
|
||||||
|
|
||||||
# Monitor server logs for API calls
|
|
||||||
tail -f /var/log/nginx/access.log | grep -E "(recaptcha-config|registration-config)"
|
|
||||||
|
|
||||||
# Check browser console for prevention messages
|
|
||||||
# Look for: [reload-prevention] messages
|
|
||||||
```
|
|
||||||
|
|
||||||
## Debugging & Monitoring
|
|
||||||
|
|
||||||
### Browser Console Commands
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Check reload loop status
|
|
||||||
window.__reloadLoopStatus = () => {
|
|
||||||
const { getReloadLoopStatus } = require('~/utils/reload-loop-prevention');
|
|
||||||
console.table(getReloadLoopStatus());
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check config cache status
|
|
||||||
window.__configCacheStatus = () => {
|
|
||||||
console.log('Config Cache:', window.__configCache);
|
|
||||||
console.log('Initialized:', window.__configCacheInitialized);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear prevention state (for testing)
|
|
||||||
window.__clearReloadPrevention = () => {
|
|
||||||
const { clearReloadLoopPrevention } = require('~/utils/reload-loop-prevention');
|
|
||||||
clearReloadLoopPrevention();
|
|
||||||
console.log('Reload loop prevention cleared');
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Server-Side Monitoring
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Monitor API call frequency
|
|
||||||
grep -E "(recaptcha-config|registration-config)" /var/log/nginx/access.log | \
|
|
||||||
awk '{print $4}' | sort | uniq -c | sort -nr
|
|
||||||
|
|
||||||
# Check for error patterns
|
|
||||||
tail -f /var/log/nginx/error.log | grep -E "(reload|loop|circuit)"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Log Messages to Monitor
|
|
||||||
|
|
||||||
**Successful Prevention:**
|
|
||||||
```
|
|
||||||
[reload-prevention] Page load allowed: signup-page (/signup)
|
|
||||||
[config-cache-init] Comprehensive config cache and reload prevention plugin initialized successfully
|
|
||||||
```
|
|
||||||
|
|
||||||
**Loop Detection:**
|
|
||||||
```
|
|
||||||
[reload-prevention] Reload loop detected for /signup (6 loads)
|
|
||||||
[reload-prevention] Page load blocked: signup-page (/signup)
|
|
||||||
[config-cache-init] Config API loop detected! /api/recaptcha-config
|
|
||||||
```
|
|
||||||
|
|
||||||
**Recovery:**
|
|
||||||
```
|
|
||||||
[reload-prevention] Emergency block lifted for /signup
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Impact
|
|
||||||
|
|
||||||
### Before Implementation
|
|
||||||
- **Mobile Safari**: 15+ page reloads, 30+ API calls
|
|
||||||
- **Load Time**: 15-30 seconds (if it ever loaded)
|
|
||||||
- **Success Rate**: <20% on mobile Safari
|
|
||||||
|
|
||||||
### After Implementation
|
|
||||||
- **Mobile Safari**: 1-2 page reloads maximum
|
|
||||||
- **Load Time**: 2-5 seconds consistently
|
|
||||||
- **Success Rate**: >95% on mobile Safari
|
|
||||||
- **API Calls**: Max 2 per config type per session
|
|
||||||
|
|
||||||
## Rollback Plan
|
|
||||||
|
|
||||||
If issues arise, remove in this order:
|
|
||||||
|
|
||||||
1. **Remove page-level checks:**
|
|
||||||
```typescript
|
|
||||||
// Comment out in onMounted functions
|
|
||||||
// const canLoad = initReloadLoopPrevention('page-name');
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Revert plugin:**
|
|
||||||
```bash
|
|
||||||
git checkout HEAD~1 -- plugins/04.config-cache-init.client.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Remove prevention utility:**
|
|
||||||
```bash
|
|
||||||
rm utils/reload-loop-prevention.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Options
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
```env
|
|
||||||
# Enable debug mode (development only)
|
|
||||||
NUXT_RELOAD_PREVENTION_DEBUG=true
|
|
||||||
|
|
||||||
# Adjust thresholds
|
|
||||||
NUXT_RELOAD_PREVENTION_THRESHOLD=5
|
|
||||||
NUXT_RELOAD_PREVENTION_WINDOW=10000
|
|
||||||
NUXT_RELOAD_PREVENTION_BLOCK_TIME=30000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Runtime Configuration
|
|
||||||
```typescript
|
|
||||||
// Adjust thresholds in utils/reload-loop-prevention.ts
|
|
||||||
const RELOAD_LOOP_THRESHOLD = 5; // Max page loads
|
|
||||||
const TIME_WINDOW = 10000; // Time window (ms)
|
|
||||||
const EMERGENCY_BLOCK_TIME = 30000; // Block duration (ms)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mobile Browser Compatibility
|
|
||||||
|
|
||||||
### Tested Browsers
|
|
||||||
- **iOS Safari**: 15.0+ ✅
|
|
||||||
- **iOS Chrome**: 110+ ✅
|
|
||||||
- **Android Chrome**: 110+ ✅
|
|
||||||
- **Android Firefox**: 115+ ✅
|
|
||||||
- **Desktop Safari**: 16+ ✅
|
|
||||||
|
|
||||||
### Browser-Specific Features
|
|
||||||
- **iOS Safari**: bfcache handling, viewport fixes
|
|
||||||
- **Android Chrome**: Performance optimizations
|
|
||||||
- **All Mobile**: Touch-friendly error UI, reduced animations
|
|
||||||
|
|
||||||
## Future Improvements
|
|
||||||
|
|
||||||
### Phase 2 Enhancements
|
|
||||||
1. **ML-Based Detection**: Learn user patterns to predict loops
|
|
||||||
2. **Service Worker Integration**: Cache configs in service worker
|
|
||||||
3. **Real-time Monitoring**: Dashboard for reload loop metrics
|
|
||||||
4. **A/B Testing**: Test different threshold values
|
|
||||||
5. **User Feedback**: Collect feedback on blocked experiences
|
|
||||||
|
|
||||||
### Performance Optimizations
|
|
||||||
1. **Config Preloading**: Preload configs during app initialization
|
|
||||||
2. **Smart Caching**: Intelligent cache invalidation
|
|
||||||
3. **Progressive Enhancement**: Load features progressively
|
|
||||||
4. **Bundle Optimization**: Lazy load prevention utilities
|
|
||||||
|
|
||||||
## Support & Maintenance
|
|
||||||
|
|
||||||
### Regular Maintenance Tasks
|
|
||||||
1. **Weekly**: Review reload loop metrics
|
|
||||||
2. **Monthly**: Analyze blocked user patterns
|
|
||||||
3. **Quarterly**: Update mobile browser compatibility
|
|
||||||
4. **Annually**: Review and optimize thresholds
|
|
||||||
|
|
||||||
### Troubleshooting Guide
|
|
||||||
|
|
||||||
**Issue: Page still reloading**
|
|
||||||
- Check console for prevention messages
|
|
||||||
- Verify plugin loading order
|
|
||||||
- Test with cleared browser cache
|
|
||||||
|
|
||||||
**Issue: False positive blocks**
|
|
||||||
- Review threshold settings
|
|
||||||
- Check for legitimate rapid navigation
|
|
||||||
- Adjust time windows if needed
|
|
||||||
|
|
||||||
**Issue: Users report blocked pages**
|
|
||||||
- Check emergency block duration
|
|
||||||
- Review user feedback channels
|
|
||||||
- Consider threshold adjustments
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This comprehensive reload loop prevention system provides:
|
|
||||||
|
|
||||||
1. **Proactive Detection**: Catches loops before they impact users
|
|
||||||
2. **Graceful Degradation**: Provides alternatives when blocking occurs
|
|
||||||
3. **Mobile Optimization**: Specifically tuned for mobile Safari issues
|
|
||||||
4. **Developer Tools**: Rich debugging and monitoring capabilities
|
|
||||||
5. **Future-Proof Architecture**: Extensible for additional features
|
|
||||||
|
|
||||||
The solution transforms the mobile Safari experience from unreliable (20% success) to highly reliable (95%+ success) while maintaining performance and user experience standards.
|
|
||||||
|
|
@ -1,325 +0,0 @@
|
||||||
# Mobile Safari & Keycloak Fixes - Complete Implementation Summary
|
|
||||||
|
|
||||||
## ✅ **Issues Successfully Resolved**
|
|
||||||
|
|
||||||
### **1. Mobile Safari Endless Reloading (Signup Page)**
|
|
||||||
**Problem:** Signup page continuously reloading on Safari iPhone
|
|
||||||
**Status:** ✅ FIXED
|
|
||||||
|
|
||||||
### **2. Keycloak "Set Your Password" 404 Error**
|
|
||||||
**Problem:** "Set Your Password" button leading to "Page not found"
|
|
||||||
**Status:** ✅ FIXED
|
|
||||||
|
|
||||||
### **3. Country Dropdown Completely Broken on Mobile**
|
|
||||||
**Problem:** Country selection dropdown overlapping with other elements, unusable interface
|
|
||||||
**Status:** ✅ FIXED
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 **Root Causes & Solutions**
|
|
||||||
|
|
||||||
### **Mobile Safari Endless Reloading Issue**
|
|
||||||
|
|
||||||
#### **Root Causes:**
|
|
||||||
1. **Performance Overload:** Heavy `backdrop-filter: blur(15px)` causing GPU strain
|
|
||||||
2. **Viewport Height Conflicts:** Incompatible `100vh` and `100dvh` units
|
|
||||||
3. **Reactive Update Loops:** Complex `onMounted()` logic triggering re-renders
|
|
||||||
4. **Background Image Performance:** Large images causing memory pressure
|
|
||||||
5. **Promise Chain Failures:** API errors bubbling up and causing page reloads
|
|
||||||
|
|
||||||
#### **Solutions Implemented:**
|
|
||||||
```typescript
|
|
||||||
// 1. Mobile Safari Detection System
|
|
||||||
utils/mobile-safari-utils.ts
|
|
||||||
- Device detection (mobile Safari, iOS, performance needs)
|
|
||||||
- Backdrop-filter disabling for problematic devices
|
|
||||||
- Viewport height optimization with CSS variables
|
|
||||||
- Performance utilities (throttle, debounce)
|
|
||||||
- Automatic CSS class application
|
|
||||||
|
|
||||||
// 2. Performance Optimizations
|
|
||||||
pages/signup.vue
|
|
||||||
- Dynamic CSS classes based on device capabilities
|
|
||||||
- Simplified onMounted() to prevent reload loops
|
|
||||||
- Better error handling that doesn't cause page reloads
|
|
||||||
- Fallback configurations to prevent undefined errors
|
|
||||||
- Mobile-specific viewport meta tag
|
|
||||||
|
|
||||||
// 3. Mobile Safari CSS Optimizations
|
|
||||||
.performance-optimized {
|
|
||||||
backdrop-filter: none; /* Remove expensive filter */
|
|
||||||
background: rgba(255, 255, 255, 0.98) !important;
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2) !important;
|
|
||||||
transition: none; /* Remove animations */
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Viewport Height Fix
|
|
||||||
.is-mobile-safari {
|
|
||||||
min-height: -webkit-fill-available;
|
|
||||||
background-attachment: scroll !important;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Keycloak "Set Your Password" 404 Error**
|
|
||||||
|
|
||||||
#### **Root Causes:**
|
|
||||||
1. **Missing Public Config:** `keycloakIssuer` not exposed to client-side
|
|
||||||
2. **Incorrect URL Structure:** Using hash fragments that don't exist
|
|
||||||
3. **Wrong Realm Name:** Using `monacousa-portal` instead of `monacousa`
|
|
||||||
|
|
||||||
#### **Solutions Implemented:**
|
|
||||||
```typescript
|
|
||||||
// 1. Fixed Nuxt Config
|
|
||||||
nuxt.config.ts
|
|
||||||
public: {
|
|
||||||
keycloakIssuer: process.env.NUXT_KEYCLOAK_ISSUER ||
|
|
||||||
"https://auth.monacousa.org/realms/monacousa"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Fixed URL Generation
|
|
||||||
pages/auth/verify-success.vue
|
|
||||||
const setupPasswordUrl = computed(() => {
|
|
||||||
const runtimeConfig = useRuntimeConfig();
|
|
||||||
const keycloakIssuer = runtimeConfig.public.keycloakIssuer ||
|
|
||||||
'https://auth.monacousa.org/realms/monacousa';
|
|
||||||
|
|
||||||
// Fixed: Remove hash fragment that caused 404
|
|
||||||
return `${keycloakIssuer}/account/`;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Country Dropdown Broken on Mobile**
|
|
||||||
|
|
||||||
#### **Root Causes:**
|
|
||||||
1. **Vuetify v-select Issues:** Mobile Safari incompatibility with complex dropdown positioning
|
|
||||||
2. **Z-index Conflicts:** Dropdown overlapping with other form elements
|
|
||||||
3. **Touch Interaction Problems:** Poor touch responsiveness on mobile devices
|
|
||||||
4. **Layout Disruption:** Dropdown breaking the form layout and rendering incorrectly
|
|
||||||
|
|
||||||
#### **Solutions Implemented:**
|
|
||||||
```typescript
|
|
||||||
// 1. Mobile-Optimized Country Selector
|
|
||||||
components/MultipleNationalityInput.vue
|
|
||||||
- Device detection to switch between desktop v-select and mobile dialog
|
|
||||||
- Full-screen country selection dialog for mobile Safari
|
|
||||||
- Touch-optimized interface with larger touch targets
|
|
||||||
- Search functionality with smooth scrolling
|
|
||||||
|
|
||||||
// 2. Mobile Dialog Interface
|
|
||||||
<v-dialog
|
|
||||||
v-model="showMobileSelector"
|
|
||||||
:fullscreen="useMobileInterface"
|
|
||||||
:transition="'dialog-bottom-transition'"
|
|
||||||
class="mobile-country-dialog"
|
|
||||||
>
|
|
||||||
<!-- Full-screen country list with search -->
|
|
||||||
<!-- Optimized for touch interaction -->
|
|
||||||
<!-- Smooth iOS-style animations -->
|
|
||||||
</v-dialog>
|
|
||||||
|
|
||||||
// 3. Performance Optimizations
|
|
||||||
- Hardware acceleration for smooth scrolling
|
|
||||||
- Disabled transitions for performance mode
|
|
||||||
- Touch-friendly 60px minimum button heights
|
|
||||||
- -webkit-overflow-scrolling: touch for iOS
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 **Files Modified**
|
|
||||||
|
|
||||||
### **New Files Created:**
|
|
||||||
- `utils/mobile-safari-utils.ts` - Mobile Safari detection and optimization utilities
|
|
||||||
- `plugins/03.mobile-safari-fixes.client.ts` - Auto-apply mobile Safari fixes
|
|
||||||
|
|
||||||
### **Files Updated:**
|
|
||||||
- `nuxt.config.ts` - Added public keycloakIssuer configuration
|
|
||||||
- `pages/signup.vue` - Complete mobile Safari optimization
|
|
||||||
- `pages/auth/verify-success.vue` - Fixed Keycloak URL + mobile Safari optimization
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 **New Features Implemented**
|
|
||||||
|
|
||||||
### **1. Device-Aware Optimization System**
|
|
||||||
```typescript
|
|
||||||
// Automatic device detection
|
|
||||||
const deviceInfo = getDeviceInfo();
|
|
||||||
const performanceMode = needsPerformanceOptimization();
|
|
||||||
const disableBackdropFilter = shouldDisableBackdropFilter();
|
|
||||||
|
|
||||||
// Dynamic CSS classes
|
|
||||||
const containerClasses = [
|
|
||||||
'base-container',
|
|
||||||
...getOptimizedClasses() // Adds: is-mobile, is-mobile-safari, performance-mode
|
|
||||||
].join(' ');
|
|
||||||
```
|
|
||||||
|
|
||||||
### **2. Progressive Performance Degradation**
|
|
||||||
- **High-performance devices:** Full visual effects (backdrop-filter, animations)
|
|
||||||
- **Mobile Safari:** Disabled backdrop-filter, simplified backgrounds
|
|
||||||
- **Performance mode:** Removed animations, lighter shadows, no transitions
|
|
||||||
|
|
||||||
### **3. Viewport Height Optimization**
|
|
||||||
```css
|
|
||||||
/* Universal viewport height handling */
|
|
||||||
.container {
|
|
||||||
min-height: 100vh;
|
|
||||||
min-height: calc(var(--vh, 1vh) * 100); /* Mobile Safari fallback */
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-mobile-safari .container {
|
|
||||||
min-height: -webkit-fill-available;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### **4. Auto-Applied Mobile Safari Fixes**
|
|
||||||
- Automatic viewport height calculation
|
|
||||||
- CSS class injection
|
|
||||||
- Resize event handling
|
|
||||||
- Route change optimization
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 **Expected Results**
|
|
||||||
|
|
||||||
### **Signup Page (Mobile Safari)**
|
|
||||||
✅ No more endless reloading
|
|
||||||
✅ Smooth performance on mobile devices
|
|
||||||
✅ Progressive visual degradation based on device capabilities
|
|
||||||
✅ Proper viewport handling without scroll issues
|
|
||||||
✅ Touch-friendly interface
|
|
||||||
|
|
||||||
### **Verification Success Page**
|
|
||||||
✅ "Set Your Password" button works correctly
|
|
||||||
✅ Proper Keycloak account management redirection
|
|
||||||
✅ Mobile Safari optimized layout
|
|
||||||
✅ Performance-optimized animations and effects
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📱 **Mobile Safari Specific Optimizations**
|
|
||||||
|
|
||||||
### **Performance Features:**
|
|
||||||
- **Disabled backdrop-filter** on mobile Safari (major performance improvement)
|
|
||||||
- **Simplified backgrounds** for low-powered devices
|
|
||||||
- **Removed heavy animations** in performance mode
|
|
||||||
- **Lighter box-shadows** and effects
|
|
||||||
- **Hardware acceleration optimizations**
|
|
||||||
|
|
||||||
### **Viewport Features:**
|
|
||||||
- **CSS custom properties** for dynamic viewport height
|
|
||||||
- **-webkit-fill-available** support for newer Safari versions
|
|
||||||
- **Resize event handling** with debouncing
|
|
||||||
- **Horizontal scroll prevention**
|
|
||||||
|
|
||||||
### **Touch Optimizations:**
|
|
||||||
- **48px minimum touch targets** for buttons
|
|
||||||
- **Optimized button spacing** on mobile
|
|
||||||
- **Touch-friendly hover states**
|
|
||||||
- **Disabled zoom** on form inputs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ **Technical Implementation Details**
|
|
||||||
|
|
||||||
### **Device Detection Logic:**
|
|
||||||
```typescript
|
|
||||||
export function getDeviceInfo(): DeviceInfo {
|
|
||||||
const userAgent = navigator.userAgent;
|
|
||||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
|
||||||
const isIOS = /iPad|iPhone|iPod/.test(userAgent);
|
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
|
|
||||||
const isMobileSafari = isIOS && isSafari;
|
|
||||||
|
|
||||||
return { isMobile, isSafari, isMobileSafari, isIOS, safariVersion };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### **CSS Performance Classes:**
|
|
||||||
```css
|
|
||||||
/* Applied automatically based on device detection */
|
|
||||||
.is-mobile { /* Mobile-specific optimizations */ }
|
|
||||||
.is-mobile-safari { /* Safari-specific fixes */ }
|
|
||||||
.is-ios { /* iOS-specific adjustments */ }
|
|
||||||
.performance-mode { /* Performance optimizations */ }
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Viewport Height Handling:**
|
|
||||||
```javascript
|
|
||||||
// Automatic viewport height calculation
|
|
||||||
const setViewportHeight = () => {
|
|
||||||
const vh = window.innerHeight * 0.01;
|
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 **Testing Checklist**
|
|
||||||
|
|
||||||
### **Mobile Safari Testing:**
|
|
||||||
- [ ] Signup page loads without endless reloading
|
|
||||||
- [ ] Form submission works correctly
|
|
||||||
- [ ] Page scrolling is smooth
|
|
||||||
- [ ] No horizontal scroll issues
|
|
||||||
- [ ] Touch targets are appropriately sized
|
|
||||||
|
|
||||||
### **Keycloak Integration Testing:**
|
|
||||||
- [ ] "Set Your Password" button redirects correctly
|
|
||||||
- [ ] Keycloak account management page loads
|
|
||||||
- [ ] Password setup process works
|
|
||||||
- [ ] Login flow continues normally after password setup
|
|
||||||
|
|
||||||
### **Cross-Device Testing:**
|
|
||||||
- [ ] Works on iPhone Safari
|
|
||||||
- [ ] Works on Android Chrome
|
|
||||||
- [ ] Works on desktop browsers
|
|
||||||
- [ ] Performance optimizations activate appropriately
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 **Performance Improvements**
|
|
||||||
|
|
||||||
### **Before Fixes:**
|
|
||||||
- Heavy backdrop-filter causing 60%+ GPU usage
|
|
||||||
- Viewport height conflicts causing layout thrashing
|
|
||||||
- Complex reactive loops causing memory leaks
|
|
||||||
- Broken Keycloak URLs causing user frustration
|
|
||||||
|
|
||||||
### **After Fixes:**
|
|
||||||
- ✅ 90%+ reduction in GPU usage on mobile Safari
|
|
||||||
- ✅ Stable viewport handling without layout shifts
|
|
||||||
- ✅ Clean initialization without reactive loops
|
|
||||||
- ✅ Working Keycloak integration with proper URLs
|
|
||||||
- ✅ Progressive performance degradation based on device capabilities
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 **Automatic Features**
|
|
||||||
|
|
||||||
The system now automatically:
|
|
||||||
1. **Detects device capabilities** on page load
|
|
||||||
2. **Applies appropriate CSS classes** for optimization
|
|
||||||
3. **Sets viewport height variables** for mobile Safari
|
|
||||||
4. **Handles resize events** with debouncing
|
|
||||||
5. **Disables performance-heavy features** on constrained devices
|
|
||||||
6. **Uses correct Keycloak URLs** based on configuration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 **Summary**
|
|
||||||
|
|
||||||
Both critical issues have been comprehensively resolved:
|
|
||||||
|
|
||||||
1. **Mobile Safari endless reloading** - Fixed with performance optimization system
|
|
||||||
2. **Keycloak 404 error** - Fixed with proper URL configuration
|
|
||||||
|
|
||||||
The MonacoUSA Portal now provides:
|
|
||||||
- ✅ Reliable mobile Safari compatibility
|
|
||||||
- ✅ Working Keycloak integration
|
|
||||||
- ✅ Performance optimization for all devices
|
|
||||||
- ✅ Progressive enhancement based on capabilities
|
|
||||||
- ✅ Future-proof architecture for mobile web development
|
|
||||||
|
|
||||||
The implementation is production-ready with comprehensive error handling, logging, and device-specific optimizations.
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
# Mobile Safari Reload Loop - Final Fix
|
|
||||||
|
|
||||||
## Problem Description
|
|
||||||
Users on Safari iPhone experienced endless reload loops on:
|
|
||||||
- Signup page (`/signup`)
|
|
||||||
- Email verification page (`/auth/verify`)
|
|
||||||
- Password setup page (`/auth/setup-password`)
|
|
||||||
|
|
||||||
The server logs showed repeated calls to:
|
|
||||||
- `/api/recaptcha-config`
|
|
||||||
- `/api/registration-config`
|
|
||||||
|
|
||||||
## Root Causes Identified
|
|
||||||
|
|
||||||
### 1. Incorrect Reactive Reference in Signup Page
|
|
||||||
**Issue**: `cardClasses` was defined as a ref containing a function instead of the function's result:
|
|
||||||
```typescript
|
|
||||||
// WRONG - causes reactivity issues
|
|
||||||
const cardClasses = ref(() => {
|
|
||||||
const classes = ['signup-card'];
|
|
||||||
// ...
|
|
||||||
return classes.join(' ');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fix**: Execute the function immediately and store the result:
|
|
||||||
```typescript
|
|
||||||
// CORRECT
|
|
||||||
const cardClasses = ref((() => {
|
|
||||||
const classes = ['signup-card'];
|
|
||||||
// ...
|
|
||||||
return classes.join(' ');
|
|
||||||
})()); // Note the immediate execution with ()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Config Cache Not Persisting Across Component Lifecycles
|
|
||||||
**Issue**: The global config cache was using module-level variables that could be reset during Vue's reactivity cycles, causing repeated API calls.
|
|
||||||
|
|
||||||
**Fix**: Use `window` object for true persistence:
|
|
||||||
```typescript
|
|
||||||
// Use window object for true persistence across component lifecycle
|
|
||||||
function getGlobalCache(): ConfigCache {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return defaultCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(window as any).__configCache) {
|
|
||||||
(window as any).__configCache = defaultCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (window as any).__configCache;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Missing Circuit Breaker Protection
|
|
||||||
**Issue**: No protection against rapid successive API calls that could trigger reload loops.
|
|
||||||
|
|
||||||
**Fix**: Implemented circuit breaker with threshold protection:
|
|
||||||
- Max 5 calls in 10-second window
|
|
||||||
- Automatic blocking when threshold reached
|
|
||||||
- Fallback to default values when blocked
|
|
||||||
|
|
||||||
## Complete Solution Implementation
|
|
||||||
|
|
||||||
### Files Modified
|
|
||||||
|
|
||||||
1. **`pages/signup.vue`**
|
|
||||||
- Fixed `cardClasses` ref definition
|
|
||||||
- Ensured static device detection
|
|
||||||
- Added initialization flag to prevent multiple setups
|
|
||||||
|
|
||||||
2. **`utils/config-cache.ts`**
|
|
||||||
- Moved cache storage to `window` object
|
|
||||||
- Added `getGlobalCache()` function for persistent storage
|
|
||||||
- Improved circuit breaker implementation
|
|
||||||
- Added proper logging for debugging
|
|
||||||
|
|
||||||
3. **`plugins/04.config-cache-init.client.ts`** (NEW)
|
|
||||||
- Pre-initializes config cache structure
|
|
||||||
- Sets up global error handlers to catch reload loops
|
|
||||||
- Prevents multiple initializations
|
|
||||||
- Adds unhandled rejection handler
|
|
||||||
|
|
||||||
## How The Fix Works
|
|
||||||
|
|
||||||
### 1. Plugin Initialization (runs first)
|
|
||||||
- `04.config-cache-init.client.ts` runs before other plugins
|
|
||||||
- Initializes `window.__configCache` structure
|
|
||||||
- Sets up error handlers to catch potential reload loops
|
|
||||||
- Marks initialization complete with `window.__configCacheInitialized`
|
|
||||||
|
|
||||||
### 2. Config Loading (on-demand)
|
|
||||||
- When pages need config, they call `loadAllConfigs()`
|
|
||||||
- Cache is checked first via `getGlobalCache()`
|
|
||||||
- If cached, returns immediately (no API call)
|
|
||||||
- If not cached, makes API call with circuit breaker protection
|
|
||||||
- Results stored in `window.__configCache` for persistence
|
|
||||||
|
|
||||||
### 3. Circuit Breaker Protection
|
|
||||||
- Tracks API call history in time windows
|
|
||||||
- Blocks calls if threshold exceeded (5 calls in 10 seconds)
|
|
||||||
- Returns fallback values when blocked
|
|
||||||
- Prevents cascade failures and reload loops
|
|
||||||
|
|
||||||
## Testing Instructions
|
|
||||||
|
|
||||||
### Test on Safari iPhone:
|
|
||||||
1. Clear Safari cache and cookies
|
|
||||||
2. Navigate to `/signup` - should load without reload loop
|
|
||||||
3. Navigate to `/auth/verify?token=test` - should show error without loop
|
|
||||||
4. Navigate to `/auth/setup-password?email=test@test.com` - should load without loop
|
|
||||||
|
|
||||||
### Monitor Console Logs:
|
|
||||||
- Look for `[config-cache-init]` messages confirming initialization
|
|
||||||
- Check for `[config-cache] Returning cached` messages on subsequent loads
|
|
||||||
- Watch for `Circuit breaker activated` if threshold reached
|
|
||||||
|
|
||||||
### Server Logs:
|
|
||||||
- Should see initial calls to `/api/recaptcha-config` and `/api/registration-config`
|
|
||||||
- Should NOT see repeated calls in quick succession
|
|
||||||
- Maximum 2-3 calls per page load (initial + retry if needed)
|
|
||||||
|
|
||||||
## Prevention Measures
|
|
||||||
|
|
||||||
### 1. Static Detection Pattern
|
|
||||||
All device detection uses static, non-reactive patterns:
|
|
||||||
```typescript
|
|
||||||
const deviceInfo = getStaticDeviceInfo(); // Called once, never reactive
|
|
||||||
const containerClasses = ref(getDeviceCssClasses('page-name')); // Computed once
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configuration Caching
|
|
||||||
All configuration loading uses cached utility:
|
|
||||||
```typescript
|
|
||||||
const configs = await loadAllConfigs(); // Uses cache automatically
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Initialization Flags
|
|
||||||
Prevent multiple initializations:
|
|
||||||
```typescript
|
|
||||||
let initialized = false;
|
|
||||||
onMounted(() => {
|
|
||||||
if (initialized) return;
|
|
||||||
initialized = true;
|
|
||||||
// ... initialization code
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
### Key Metrics to Watch:
|
|
||||||
1. **API Call Frequency**: `/api/recaptcha-config` and `/api/registration-config` should be called max once per session
|
|
||||||
2. **Page Load Time**: Should be under 2 seconds on mobile
|
|
||||||
3. **Error Rate**: No "Maximum call stack" or recursion errors
|
|
||||||
4. **User Reports**: No complaints about infinite loading
|
|
||||||
|
|
||||||
### Debug Commands:
|
|
||||||
```javascript
|
|
||||||
// Check cache status in browser console
|
|
||||||
console.log(window.__configCache);
|
|
||||||
console.log(window.__configCacheInitialized);
|
|
||||||
|
|
||||||
// Force clear cache (for testing)
|
|
||||||
window.__configCache = null;
|
|
||||||
window.__configCacheInitialized = false;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rollback Plan
|
|
||||||
|
|
||||||
If issues persist, rollback changes:
|
|
||||||
1. Remove `plugins/04.config-cache-init.client.ts`
|
|
||||||
2. Revert `utils/config-cache.ts` to previous version
|
|
||||||
3. Revert `pages/signup.vue` changes
|
|
||||||
|
|
||||||
## Long-term Improvements
|
|
||||||
|
|
||||||
1. **Server-side caching**: Cache config in Redis/memory on server
|
|
||||||
2. **SSR config injection**: Inject config during SSR to avoid client calls
|
|
||||||
3. **PWA service worker**: Cache config in service worker
|
|
||||||
4. **Config versioning**: Add version check to invalidate stale cache
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The mobile Safari reload loop has been resolved through:
|
|
||||||
1. Fixing reactive reference bugs
|
|
||||||
2. Implementing proper persistent caching
|
|
||||||
3. Adding circuit breaker protection
|
|
||||||
4. Setting up global error handlers
|
|
||||||
|
|
||||||
The solution is backward compatible and doesn't affect desktop users or other browsers. The fix specifically targets the root causes while maintaining the existing functionality.
|
|
||||||
|
|
@ -1,274 +0,0 @@
|
||||||
# 🔄 Mobile Safari Reload Loop Fix - Implementation Complete
|
|
||||||
|
|
||||||
## 🎯 Executive Summary
|
|
||||||
|
|
||||||
**SUCCESS!** The endless reload loops on mobile Safari for the signup, email verification, and password reset pages have been **completely eliminated** by replacing reactive mobile detection with static, non-reactive alternatives.
|
|
||||||
|
|
||||||
### ✅ Root Cause Identified & Fixed
|
|
||||||
- **Problem**: Reactive `useMobileDetection` composable with global state that updated `viewportHeight` on every viewport change
|
|
||||||
- **Result**: ALL components using the composable re-rendered simultaneously when mobile Safari viewport changed (virtual keyboard, touch, scroll)
|
|
||||||
- **Solution**: Replaced with official @nuxt/device module and static detection patterns
|
|
||||||
|
|
||||||
### ✅ Key Benefits Achieved
|
|
||||||
- **🚀 No More Reload Loops**: Eliminated reactive cascade that caused infinite re-renders
|
|
||||||
- **📱 Better Mobile Performance**: Static detection runs once vs. continuous reactive updates
|
|
||||||
- **🔧 Professional Solution**: Using official @nuxt/device module (Trust Score 9.1) instead of custom reactive code
|
|
||||||
- **🧹 Cleaner Architecture**: Removed complex reactive state management for simple static detection
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Implementation Phases Completed
|
|
||||||
|
|
||||||
### ✅ Phase 1: Architecture Analysis
|
|
||||||
- **Status**: Complete
|
|
||||||
- **Finding**: Confirmed `useMobileDetection` reactive global state as root cause
|
|
||||||
- **Evidence**: `globalState.viewportHeight` updates triggered cascading re-renders
|
|
||||||
|
|
||||||
### ✅ Phase 2: Install Nuxt Device Module
|
|
||||||
- **Status**: Complete
|
|
||||||
- **Action**: `npx nuxi@latest module add device`
|
|
||||||
- **Result**: Official @nuxtjs/device@3.2.4 installed successfully
|
|
||||||
|
|
||||||
### ✅ Phase 3: Migrate Signup Page
|
|
||||||
- **Status**: Complete
|
|
||||||
- **Changes**:
|
|
||||||
- Removed `useMobileDetection()` reactive composable
|
|
||||||
- Replaced `computed()` classes with static `ref()`
|
|
||||||
- Used `useDevice()` from Nuxt Device Module in `onMounted()` only
|
|
||||||
- **Result**: No more reactive subscriptions = No reload loops
|
|
||||||
|
|
||||||
### ✅ Phase 4: Migrate Setup Password Page
|
|
||||||
- **Status**: Complete
|
|
||||||
- **Changes**: Same pattern as signup page
|
|
||||||
- **Result**: Static device detection, no reactive dependencies
|
|
||||||
|
|
||||||
### ✅ Phase 5: Email Verification Page
|
|
||||||
- **Status**: Complete (Already had static detection)
|
|
||||||
- **Verification**: Confirmed no reactive mobile detection usage
|
|
||||||
|
|
||||||
### ✅ Phase 6: Migrate Mobile Safari Plugin
|
|
||||||
- **Status**: Complete
|
|
||||||
- **Changes**:
|
|
||||||
- Removed `useMobileDetection()` import
|
|
||||||
- Replaced with static user agent parsing
|
|
||||||
- No reactive subscriptions, runs once on plugin init
|
|
||||||
- **Result**: Initial mobile Safari fixes without reactive state
|
|
||||||
|
|
||||||
### ✅ Phase 7: CSS-Only Viewport Management
|
|
||||||
- **Status**: Complete
|
|
||||||
- **New File**: `utils/viewport-manager.ts`
|
|
||||||
- **Features**:
|
|
||||||
- Updates `--vh` CSS custom property only (no Vue reactivity)
|
|
||||||
- Smart keyboard detection to prevent unnecessary updates
|
|
||||||
- Mobile Safari specific optimizations
|
|
||||||
- Auto-initializes on client side
|
|
||||||
|
|
||||||
### ✅ Phase 8: Testing & Validation
|
|
||||||
- **Status**: 🔄 **Ready for User Testing**
|
|
||||||
- **Test Plan**: See Testing Instructions below
|
|
||||||
|
|
||||||
### ✅ Phase 9: Dependency Analysis & Research
|
|
||||||
- **Status**: Complete
|
|
||||||
- **Result**: Identified @nuxt/device as optimal solution
|
|
||||||
- **Benefits**: Official support, no reactive state, better performance
|
|
||||||
|
|
||||||
### ✅ Phase 10: Legacy Code Cleanup
|
|
||||||
- **Status**: **COMPLETE** ✅
|
|
||||||
- **Files Removed**:
|
|
||||||
- `composables/useMobileDetection.ts` (reactive composable causing reload loops)
|
|
||||||
- `utils/mobile-safari-utils.ts` (redundant utility functions)
|
|
||||||
- **Result**: Cleaner codebase using official @nuxt/device module
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Technical Implementation Details
|
|
||||||
|
|
||||||
### Before (Problematic Reactive Pattern):
|
|
||||||
```typescript
|
|
||||||
// ❌ OLD: Reactive global state that caused reload loops
|
|
||||||
const mobileDetection = useMobileDetection();
|
|
||||||
const containerClasses = computed(() => {
|
|
||||||
const classes = ['signup-container'];
|
|
||||||
if (mobileDetection.isMobile) classes.push('is-mobile');
|
|
||||||
return classes.join(' '); // Re-runs on every viewport change!
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### After (Static Non-Reactive Pattern):
|
|
||||||
```typescript
|
|
||||||
// ✅ NEW: Static device detection, no reactive dependencies
|
|
||||||
const { isMobile, isIos, isSafari } = useDevice();
|
|
||||||
const containerClasses = ref('signup-container');
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const classes = ['signup-container'];
|
|
||||||
if (isMobile) classes.push('is-mobile');
|
|
||||||
if (isMobile && isIos && isSafari) classes.push('is-mobile-safari');
|
|
||||||
containerClasses.value = classes.join(' '); // Runs once only!
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Changes Made:
|
|
||||||
|
|
||||||
#### 1. **pages/signup.vue**
|
|
||||||
- ✅ Removed reactive `useMobileDetection()`
|
|
||||||
- ✅ Replaced `computed()` with static `ref()`
|
|
||||||
- ✅ Added `useDevice()` in `onMounted()` for static detection
|
|
||||||
- ✅ Fixed TypeScript issues with device property names
|
|
||||||
|
|
||||||
#### 2. **pages/auth/setup-password.vue**
|
|
||||||
- ✅ Same pattern as signup page
|
|
||||||
- ✅ Simplified password visibility toggle (no mobile-specific reactive logic)
|
|
||||||
- ✅ Static device detection in `onMounted()`
|
|
||||||
|
|
||||||
#### 3. **pages/auth/verify.vue**
|
|
||||||
- ✅ Already had static detection (confirmed no issues)
|
|
||||||
|
|
||||||
#### 4. **plugins/03.mobile-safari-fixes.client.ts**
|
|
||||||
- ✅ Removed `useMobileDetection()` import
|
|
||||||
- ✅ Replaced with static user agent parsing
|
|
||||||
- ✅ No reactive subscriptions, runs once only
|
|
||||||
|
|
||||||
#### 5. **utils/viewport-manager.ts** (New)
|
|
||||||
- ✅ CSS-only viewport height management
|
|
||||||
- ✅ Updates `--vh` custom property without Vue reactivity
|
|
||||||
- ✅ Smart keyboard detection and debouncing
|
|
||||||
- ✅ Mobile Safari specific optimizations
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Testing Instructions
|
|
||||||
|
|
||||||
### Phase 8: User Testing Required
|
|
||||||
|
|
||||||
**Please test the following on mobile Safari (iPhone):**
|
|
||||||
|
|
||||||
#### 1. **Signup Page** (`/signup`)
|
|
||||||
- ✅ **Before**: Endless reload loops when interacting with form
|
|
||||||
- 🔄 **Test Now**: Should load normally, no reloads when:
|
|
||||||
- Opening virtual keyboard
|
|
||||||
- Scrolling the page
|
|
||||||
- Rotating device
|
|
||||||
- Touching form fields
|
|
||||||
- Filling out the form
|
|
||||||
|
|
||||||
#### 2. **Email Verification Links**
|
|
||||||
- ✅ **Before**: Reload loops when clicking verification emails
|
|
||||||
- 🔄 **Test Now**: Should work normally:
|
|
||||||
- Click verification link from email
|
|
||||||
- Should navigate to verify page without loops
|
|
||||||
- Should process verification and redirect to success page
|
|
||||||
|
|
||||||
#### 3. **Password Setup** (`/auth/setup-password`)
|
|
||||||
- ✅ **Before**: Reload loops on password setup page
|
|
||||||
- 🔄 **Test Now**: Should work normally:
|
|
||||||
- Load page from email link
|
|
||||||
- Interact with password fields
|
|
||||||
- Toggle password visibility
|
|
||||||
- Submit password form
|
|
||||||
|
|
||||||
#### 4. **Mobile Safari Optimizations Still Work**
|
|
||||||
- 🔄 **Verify**: CSS `--vh` variable updates correctly
|
|
||||||
- 🔄 **Verify**: Mobile classes still applied (`.is-mobile`, `.is-mobile-safari`)
|
|
||||||
- 🔄 **Verify**: Viewport changes handled properly
|
|
||||||
- 🔄 **Verify**: No console errors
|
|
||||||
|
|
||||||
### Testing Checklist:
|
|
||||||
- [ ] Signup page loads without reload loops
|
|
||||||
- [ ] Email verification links work normally
|
|
||||||
- [ ] Password setup works without issues
|
|
||||||
- [ ] Mobile Safari optimizations still functional
|
|
||||||
- [ ] No console errors in browser dev tools
|
|
||||||
- [ ] Form interactions work smoothly
|
|
||||||
- [ ] Virtual keyboard doesn't cause reloads
|
|
||||||
- [ ] Device rotation handled properly
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Performance Improvements
|
|
||||||
|
|
||||||
### Before Fix:
|
|
||||||
- 🔴 **Reactive State**: Global state updated on every viewport change
|
|
||||||
- 🔴 **Component Re-renders**: ALL components using composable re-rendered simultaneously
|
|
||||||
- 🔴 **Viewport Events**: High-frequency updates caused cascading effects
|
|
||||||
- 🔴 **Mobile Safari**: Extreme viewport sensitivity triggered continuous loops
|
|
||||||
|
|
||||||
### After Fix:
|
|
||||||
- 🟢 **Static Detection**: Device detection runs once per page load
|
|
||||||
- 🟢 **No Re-renders**: Classes applied statically, no reactive dependencies
|
|
||||||
- 🟢 **CSS-Only Updates**: Viewport changes update CSS properties only
|
|
||||||
- 🟢 **Optimized Mobile**: Smart debouncing and keyboard detection
|
|
||||||
|
|
||||||
### Measured Benefits:
|
|
||||||
- **🚀 Zero Reload Loops**: Complete elimination of the core issue
|
|
||||||
- **📱 Better Performance**: Significantly reduced re-rendering overhead
|
|
||||||
- **🔧 Simpler Code**: Less complex reactive state management
|
|
||||||
- **💪 Official Support**: Using well-tested @nuxt/device module
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Solution Architecture
|
|
||||||
|
|
||||||
### Component Layer:
|
|
||||||
```
|
|
||||||
📱 Pages (signup, setup-password, verify)
|
|
||||||
├── useDevice() - Static detection from @nuxt/device
|
|
||||||
├── onMounted() - Apply classes once, no reactivity
|
|
||||||
└── ref() containers - Static class strings
|
|
||||||
```
|
|
||||||
|
|
||||||
### System Layer:
|
|
||||||
```
|
|
||||||
🔧 Plugin Layer (mobile-safari-fixes)
|
|
||||||
├── Static user agent parsing
|
|
||||||
├── One-time initialization
|
|
||||||
└── No reactive subscriptions
|
|
||||||
|
|
||||||
📐 Viewport Management (viewport-manager.ts)
|
|
||||||
├── CSS custom property updates only
|
|
||||||
├── Smart keyboard detection
|
|
||||||
├── Debounced resize handling
|
|
||||||
└── No Vue component reactivity
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefits:
|
|
||||||
- **🎯 Targeted**: Mobile Safari specific optimizations without affecting other browsers
|
|
||||||
- **🔒 Isolated**: No cross-component reactive dependencies
|
|
||||||
- **⚡ Performant**: Static detection vs. continuous reactive updates
|
|
||||||
- **🧹 Clean**: Uses official modules vs. custom reactive code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Next Steps
|
|
||||||
|
|
||||||
### Immediate:
|
|
||||||
1. **🧪 User Testing**: Test all affected pages on mobile Safari iPhone
|
|
||||||
2. **✅ Validation**: Confirm reload loops are eliminated
|
|
||||||
3. **🔍 Verification**: Ensure mobile optimizations still work
|
|
||||||
|
|
||||||
### ✅ Cleanup Complete:
|
|
||||||
1. **🧹 Cleanup**: ✅ **DONE** - Removed legacy reactive mobile detection files
|
|
||||||
2. **📝 Documentation**: ✅ **DONE** - Implementation document updated
|
|
||||||
3. **🎉 Deployment**: Ready for production deployment with confidence
|
|
||||||
|
|
||||||
### Rollback Plan (if needed):
|
|
||||||
- All original files are preserved
|
|
||||||
- Can revert individual components if issues found
|
|
||||||
- Plugin and viewport manager are additive (can be disabled)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎊 Success Metrics
|
|
||||||
|
|
||||||
This implementation successfully addresses:
|
|
||||||
|
|
||||||
- ✅ **Primary Issue**: Mobile Safari reload loops completely eliminated
|
|
||||||
- ✅ **Performance**: Significantly reduced component re-rendering
|
|
||||||
- ✅ **Maintainability**: Using official @nuxt/device module vs custom reactive code
|
|
||||||
- ✅ **Architecture**: Clean separation of concerns, no reactive cascade
|
|
||||||
- ✅ **Mobile UX**: All mobile Safari optimizations preserved
|
|
||||||
- ✅ **Compatibility**: No impact on other browsers or desktop experience
|
|
||||||
|
|
||||||
The MonacoUSA Portal signup, email verification, and password reset flows now work reliably on mobile Safari without any reload loop issues.
|
|
||||||
|
|
||||||
**🎯 Mission Accomplished!** 🎯
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
# MonacoUSA Portal Issues - Complete Fix Summary
|
|
||||||
|
|
||||||
## 🎯 **Issues Resolved**
|
|
||||||
|
|
||||||
### ✅ **Phase 1: Docker Template Inclusion (CRITICAL)**
|
|
||||||
**Problem:** Email templates not included in Docker production builds, causing all email functionality to fail.
|
|
||||||
|
|
||||||
**Solution Implemented:**
|
|
||||||
- **File Modified:** `Dockerfile`
|
|
||||||
- **Change:** Added `COPY --from=build /app/server/templates /app/server/templates`
|
|
||||||
- **Impact:** Email templates now available in production container
|
|
||||||
- **Status:** ✅ FIXED
|
|
||||||
|
|
||||||
### ✅ **Phase 2: Portal Account Detection Bug (MODERATE)**
|
|
||||||
**Problem:** User portal accounts not being detected properly - showing "No Portal Account" when account exists.
|
|
||||||
|
|
||||||
**Solution Implemented:**
|
|
||||||
- **File Modified:** `server/utils/nocodb.ts`
|
|
||||||
- **Changes:**
|
|
||||||
- Added `'Keycloak ID': 'keycloak_id'` to readFieldMap
|
|
||||||
- Added `'keycloak_id': 'keycloak_id'` to readFieldMap
|
|
||||||
- Added `'keycloak_id': 'Keycloak ID'` to writeFieldMap
|
|
||||||
- **Impact:** Portal account status now displays correctly
|
|
||||||
- **Status:** ✅ FIXED
|
|
||||||
|
|
||||||
### ✅ **Phase 3: Enhanced Member Deletion with Keycloak Cleanup (IMPORTANT)**
|
|
||||||
**Problem:** Member deletion only removed NocoDB records, leaving orphaned Keycloak accounts.
|
|
||||||
|
|
||||||
**Solution Implemented:**
|
|
||||||
- **Files Modified:**
|
|
||||||
- `server/utils/keycloak-admin.ts` - Added `deleteKeycloakUser()` helper function
|
|
||||||
- `server/api/members/[id].delete.ts` - Enhanced deletion logic
|
|
||||||
- **Changes:**
|
|
||||||
- Retrieve member data before deletion to check for keycloak_id
|
|
||||||
- If keycloak_id exists, delete Keycloak user first
|
|
||||||
- Continue with NocoDB deletion regardless of Keycloak result
|
|
||||||
- Enhanced logging and error handling
|
|
||||||
- **Impact:** Complete data cleanup on member deletion
|
|
||||||
- **Status:** ✅ FIXED
|
|
||||||
|
|
||||||
## 🚀 **Implementation Details**
|
|
||||||
|
|
||||||
### Docker Template Fix
|
|
||||||
```dockerfile
|
|
||||||
# Added to Dockerfile
|
|
||||||
COPY --from=build /app/server/templates /app/server/templates
|
|
||||||
```
|
|
||||||
|
|
||||||
### Portal Account Detection Fix
|
|
||||||
```javascript
|
|
||||||
// Added to field mappings in nocodb.ts
|
|
||||||
'Keycloak ID': 'keycloak_id',
|
|
||||||
'keycloak_id': 'keycloak_id',
|
|
||||||
// ... in readFieldMap
|
|
||||||
|
|
||||||
'keycloak_id': 'Keycloak ID'
|
|
||||||
// ... in writeFieldMap
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enhanced Member Deletion
|
|
||||||
```javascript
|
|
||||||
// New helper function
|
|
||||||
export async function deleteKeycloakUser(userId: string): Promise<void>
|
|
||||||
|
|
||||||
// Enhanced deletion logic
|
|
||||||
1. Get member data to check keycloak_id
|
|
||||||
2. If keycloak_id exists, delete Keycloak user
|
|
||||||
3. Delete NocoDB record
|
|
||||||
4. Log completion status
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 **Impact Summary**
|
|
||||||
|
|
||||||
| Issue | Severity | Status | Impact |
|
|
||||||
|-------|----------|---------|--------|
|
|
||||||
| Docker Templates | CRITICAL | ✅ FIXED | Email functionality restored |
|
|
||||||
| Portal Detection | MODERATE | ✅ FIXED | UX improved, accounts display correctly |
|
|
||||||
| Deletion Cleanup | IMPORTANT | ✅ FIXED | Data integrity maintained |
|
|
||||||
|
|
||||||
## 🧪 **Testing Recommendations**
|
|
||||||
|
|
||||||
### Phase 1 Testing (Docker Templates)
|
|
||||||
1. Rebuild Docker container
|
|
||||||
2. Check production logs for template loading
|
|
||||||
3. Test email functionality:
|
|
||||||
- Create portal account (should send welcome email)
|
|
||||||
- Test email verification
|
|
||||||
- Test password reset
|
|
||||||
|
|
||||||
### Phase 2 Testing (Portal Detection)
|
|
||||||
1. Check member list for users with portal accounts
|
|
||||||
2. Verify "Portal Account Active" chips display correctly
|
|
||||||
3. Test with your own account
|
|
||||||
|
|
||||||
### Phase 3 Testing (Enhanced Deletion)
|
|
||||||
1. Create test member with portal account
|
|
||||||
2. Delete member from admin panel
|
|
||||||
3. Check logs for both NocoDB and Keycloak deletion
|
|
||||||
4. Verify no orphaned accounts remain
|
|
||||||
|
|
||||||
## 🔍 **Monitoring & Logging**
|
|
||||||
|
|
||||||
All fixes include comprehensive logging:
|
|
||||||
- Docker template loading logged at container startup
|
|
||||||
- Portal account detection logged during member list retrieval
|
|
||||||
- Enhanced deletion logs both NocoDB and Keycloak operations
|
|
||||||
|
|
||||||
## 🛡️ **Error Handling**
|
|
||||||
|
|
||||||
- **Docker:** If templates fail to load, detailed error messages
|
|
||||||
- **Portal Detection:** Graceful fallback to existing data
|
|
||||||
- **Enhanced Deletion:** Continues NocoDB deletion even if Keycloak fails
|
|
||||||
|
|
||||||
## ✨ **Additional Improvements**
|
|
||||||
|
|
||||||
- Better error messages and status reporting
|
|
||||||
- Comprehensive logging for debugging
|
|
||||||
- Graceful handling of edge cases
|
|
||||||
- Maintains backwards compatibility
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**All critical issues have been resolved!** The MonacoUSA Portal now has:
|
|
||||||
- ✅ Working email functionality in production
|
|
||||||
- ✅ Accurate portal account status display
|
|
||||||
- ✅ Complete member deletion with proper cleanup
|
|
||||||
- ✅ Correct membership fee amount (€150/year)
|
|
||||||
- ✅ Fixed email verification links pointing to correct domain
|
|
||||||
|
|
||||||
## 🔧 **Additional Fixes Applied (Phase 4)**
|
|
||||||
|
|
||||||
### **Issue 4: Incorrect Membership Fee Amount**
|
|
||||||
**Problem:** Welcome email showed €50/year instead of €150/year
|
|
||||||
**Fix:** Updated `server/templates/welcome.hbs`
|
|
||||||
**Status:** ✅ FIXED
|
|
||||||
|
|
||||||
### **Issue 5: 404 Error on Email Verification**
|
|
||||||
**Problem:** Verification links pointed to `monacousa.org` instead of `portal.monacousa.org`
|
|
||||||
**Fix:** Updated `nuxt.config.ts` domain configuration
|
|
||||||
**Status:** ✅ FIXED
|
|
||||||
|
|
||||||
The fixes are production-ready and include proper error handling and logging.
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
# PWA Disable Test - Mobile Safari Reload Loop Fix
|
|
||||||
|
|
||||||
## Changes Made
|
|
||||||
|
|
||||||
### 1. Disabled PWA Module (`nuxt.config.ts`)
|
|
||||||
- Commented out `@vite-pwa/nuxt` module configuration
|
|
||||||
- This eliminates service worker registration
|
|
||||||
- Removes automatic updates and periodic sync
|
|
||||||
|
|
||||||
### 2. Disabled Service Worker Unregistration (`plugins/02.unregister-sw.client.ts`)
|
|
||||||
- Commented out the service worker unregistration logic
|
|
||||||
- Added logging to confirm plugin is disabled
|
|
||||||
|
|
||||||
## Root Cause Theory
|
|
||||||
|
|
||||||
**Service Worker Registration/Unregistration Conflict:**
|
|
||||||
1. PWA module tries to register service worker
|
|
||||||
2. Unregister plugin immediately removes it
|
|
||||||
3. PWA module detects missing worker and re-registers
|
|
||||||
4. Mobile Safari gets confused and reloads page
|
|
||||||
5. **Infinite loop!**
|
|
||||||
|
|
||||||
## Testing Instructions
|
|
||||||
|
|
||||||
### Mobile Safari Test (iPhone/iPad)
|
|
||||||
1. Clear Safari cache and cookies
|
|
||||||
2. Navigate to these pages and verify NO reload loops:
|
|
||||||
- `/signup`
|
|
||||||
- `/auth/verify?token=test`
|
|
||||||
- `/auth/setup-password?email=test@test.com`
|
|
||||||
|
|
||||||
### Expected Results
|
|
||||||
- **Before**: Endless page reloads, never fully loads
|
|
||||||
- **After**: Pages load normally within 2-5 seconds
|
|
||||||
|
|
||||||
### Console Logs to Look For
|
|
||||||
```
|
|
||||||
🚫 Service worker unregistration plugin disabled (PWA testing)
|
|
||||||
```
|
|
||||||
|
|
||||||
### What's Lost (Temporarily)
|
|
||||||
- PWA installation capability
|
|
||||||
- Offline functionality
|
|
||||||
- Service worker caching
|
|
||||||
- App-like behavior on mobile
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
### If This Fixes the Issue:
|
|
||||||
1. **Option A**: Keep PWA disabled (simplest)
|
|
||||||
2. **Option B**: Configure PWA properly:
|
|
||||||
- Remove service worker unregistration plugin
|
|
||||||
- Change `registerType` from `autoUpdate` to `prompt`
|
|
||||||
- Disable `periodicSyncForUpdates`
|
|
||||||
- Add proper service worker lifecycle handling
|
|
||||||
|
|
||||||
### If Issue Persists:
|
|
||||||
- Check for other causes:
|
|
||||||
- CSS backdrop-filter issues
|
|
||||||
- Large background images
|
|
||||||
- Vue reactivity loops
|
|
||||||
- Plugin conflicts
|
|
||||||
|
|
||||||
## Re-enabling PWA (If Issue is Fixed)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In nuxt.config.ts - Better PWA configuration
|
|
||||||
["@vite-pwa/nuxt", {
|
|
||||||
registerType: 'prompt', // User-initiated instead of auto
|
|
||||||
workbox: {
|
|
||||||
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
|
|
||||||
navigateFallback: '/',
|
|
||||||
navigateFallbackDenylist: [/^\/api\//]
|
|
||||||
},
|
|
||||||
client: {
|
|
||||||
installPrompt: true,
|
|
||||||
periodicSyncForUpdates: false // Disable automatic sync
|
|
||||||
},
|
|
||||||
devOptions: {
|
|
||||||
enabled: false, // Disable in development
|
|
||||||
suppressWarnings: true
|
|
||||||
}
|
|
||||||
// ... rest of manifest config
|
|
||||||
}]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rollback Instructions
|
|
||||||
|
|
||||||
If you need to revert these changes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Restore nuxt.config.ts
|
|
||||||
git checkout HEAD -- nuxt.config.ts
|
|
||||||
|
|
||||||
# Restore service worker plugin
|
|
||||||
git checkout HEAD -- plugins/02.unregister-sw.client.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Results
|
|
||||||
|
|
||||||
**Date**: _______________
|
|
||||||
**Device**: _______________
|
|
||||||
**Browser**: _______________
|
|
||||||
|
|
||||||
- [ ] `/signup` loads without reload loop
|
|
||||||
- [ ] `/auth/verify` loads without reload loop
|
|
||||||
- [ ] `/auth/setup-password` loads without reload loop
|
|
||||||
- [ ] Form submission works normally
|
|
||||||
- [ ] Navigation between pages works normally
|
|
||||||
|
|
||||||
**Notes**:
|
|
||||||
_________________________________
|
|
||||||
_________________________________
|
|
||||||
_________________________________
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This simple fix eliminates the service worker conflict that was likely causing the mobile Safari reload loops. If this resolves the issue, we can either keep PWA disabled or implement a proper PWA configuration that doesn't conflict with page loading.
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
# Safari iOS Reload Loop Fix - Complete Implementation
|
|
||||||
|
|
||||||
## Problem Solved
|
|
||||||
|
|
||||||
Fixed the endless reload loops on Safari iOS for three critical pages:
|
|
||||||
- **Signup page** (`/signup`) - Primary issue causing repeated API calls
|
|
||||||
- **Email verification page** (`/auth/verify`)
|
|
||||||
- **Password setup page** (`/auth/setup-password`)
|
|
||||||
|
|
||||||
The logs showed repeated API calls to `/api/recaptcha-config` and `/api/registration-config` causing infinite reload cycles.
|
|
||||||
|
|
||||||
## Root Cause Analysis
|
|
||||||
|
|
||||||
The reload loops were caused by **Vue reactivity cycles** that triggered Safari iOS's aggressive memory management:
|
|
||||||
|
|
||||||
1. **useDevice()** created reactive dependencies that triggered re-renders
|
|
||||||
2. **API calls in onMounted()** updated reactive refs, causing more re-renders
|
|
||||||
3. **Safari iOS memory management** interpreted frequent re-renders as memory pressure
|
|
||||||
4. **Component unmounting/remounting** created infinite loops
|
|
||||||
|
|
||||||
## Solution Implementation
|
|
||||||
|
|
||||||
### 1. Created Static Device Detection Utility
|
|
||||||
|
|
||||||
**File:** `utils/static-device-detection.ts`
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
- Non-reactive device detection using `navigator.userAgent`
|
|
||||||
- Cached results to prevent multiple parsing
|
|
||||||
- Mobile Safari specific optimization functions
|
|
||||||
- Static CSS class generation
|
|
||||||
- Functions: `getStaticDeviceInfo()`, `getDeviceCssClasses()`, `applyMobileSafariOptimizations()`
|
|
||||||
|
|
||||||
### 2. Created Global Configuration Cache
|
|
||||||
|
|
||||||
**File:** `utils/config-cache.ts`
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
- Singleton pattern preventing repeated API calls
|
|
||||||
- Circuit breaker (max 5 calls per 10 seconds)
|
|
||||||
- Proper error handling with fallback configurations
|
|
||||||
- Functions: `getCachedRecaptchaConfig()`, `getCachedRegistrationConfig()`, `loadAllConfigs()`
|
|
||||||
|
|
||||||
### 3. Fixed Signup Page
|
|
||||||
|
|
||||||
**File:** `pages/signup.vue`
|
|
||||||
|
|
||||||
**Critical Changes:**
|
|
||||||
- **Switched to reCAPTCHA v2** (checkbox style) from v3
|
|
||||||
- **Eliminated useDevice()** reactive dependencies
|
|
||||||
- **Used static device detection**
|
|
||||||
- **Implemented cached config loading**
|
|
||||||
- **Added initialization guards** to prevent multiple API calls
|
|
||||||
- **Applied mobile Safari optimizations**
|
|
||||||
|
|
||||||
### 4. Fixed Auth Pages
|
|
||||||
|
|
||||||
**Files:** `pages/auth/verify.vue`, `pages/auth/setup-password.vue`
|
|
||||||
|
|
||||||
**Changes Applied:**
|
|
||||||
- Replaced `useDevice()` with static detection
|
|
||||||
- Added mobile Safari optimizations
|
|
||||||
- Removed reactive dependencies from initialization
|
|
||||||
- Maintained existing functionality with better performance
|
|
||||||
|
|
||||||
## reCAPTCHA v2 Implementation
|
|
||||||
|
|
||||||
The signup page now uses **reCAPTCHA v2** (checkbox style) instead of v3:
|
|
||||||
|
|
||||||
### Benefits:
|
|
||||||
- ✅ **No background JavaScript execution** (unlike v3)
|
|
||||||
- ✅ **Static widget** that doesn't trigger reactive cycles
|
|
||||||
- ✅ **User-initiated** - only activates when clicked
|
|
||||||
- ✅ **No automatic token generation** that could cause loops
|
|
||||||
|
|
||||||
### Required Action:
|
|
||||||
**You need to update your reCAPTCHA configuration** with the v2 site key you created:
|
|
||||||
|
|
||||||
1. Update your environment variables with the new reCAPTCHA v2 keys:
|
|
||||||
```env
|
|
||||||
NUXT_RECAPTCHA_SITE_KEY=your-new-recaptcha-v2-site-key
|
|
||||||
NUXT_RECAPTCHA_SECRET_KEY=your-new-recaptcha-v2-secret-key
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Update the admin configuration in your portal dashboard
|
|
||||||
|
|
||||||
## Technical Implementation Details
|
|
||||||
|
|
||||||
### Static vs Reactive Detection
|
|
||||||
|
|
||||||
**Before (Problematic):**
|
|
||||||
```typescript
|
|
||||||
const { isMobile, isIos, isSafari } = useDevice(); // Creates reactive dependencies
|
|
||||||
const containerClasses = ref('signup-container'); // Reactive ref
|
|
||||||
```
|
|
||||||
|
|
||||||
**After (Fixed):**
|
|
||||||
```typescript
|
|
||||||
const deviceInfo = getStaticDeviceInfo(); // Static, cached
|
|
||||||
const containerClasses = ref(getDeviceCssClasses('signup-container')); // Computed once
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Call Prevention
|
|
||||||
|
|
||||||
**Before (Problematic):**
|
|
||||||
```typescript
|
|
||||||
$fetch('/api/recaptcha-config').then((response) => {
|
|
||||||
recaptchaConfig.value = response.data; // Reactive update triggers re-render
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**After (Fixed):**
|
|
||||||
```typescript
|
|
||||||
const configs = await loadAllConfigs(); // Cached, singleton pattern
|
|
||||||
recaptchaSiteKey = configs.recaptcha?.siteKey; // Static assignment
|
|
||||||
```
|
|
||||||
|
|
||||||
### Circuit Breaker Protection
|
|
||||||
|
|
||||||
The config cache includes circuit breaker protection:
|
|
||||||
- **Maximum 5 API calls per 10-second window**
|
|
||||||
- **Automatic fallback to default configurations**
|
|
||||||
- **Prevents API spam that was visible in logs**
|
|
||||||
|
|
||||||
## Performance Optimizations
|
|
||||||
|
|
||||||
### Mobile Safari Specific:
|
|
||||||
- **Disabled backdrop filters** (expensive CSS operations)
|
|
||||||
- **Reduced box shadows** for better performance
|
|
||||||
- **Disabled CSS transitions** on mobile Safari
|
|
||||||
- **Applied hardware acceleration optimizations**
|
|
||||||
- **Set proper viewport height** using CSS variables
|
|
||||||
|
|
||||||
### Memory Management:
|
|
||||||
- **Eliminated reactive watchers** during initialization
|
|
||||||
- **Static class computation** prevents re-calculations
|
|
||||||
- **Proper component cleanup** on unmount
|
|
||||||
- **Initialization guards** prevent duplicate setup
|
|
||||||
|
|
||||||
## Testing Recommendations
|
|
||||||
|
|
||||||
### 1. Manual Testing on Safari iOS:
|
|
||||||
1. **Signup Page:** Verify no reload loops, reCAPTCHA v2 checkbox appears
|
|
||||||
2. **Email Verification:** Test email verification links work smoothly
|
|
||||||
3. **Password Setup:** Test password setup from email links
|
|
||||||
|
|
||||||
### 2. Monitor Server Logs:
|
|
||||||
- **No repeated API calls** to `/api/recaptcha-config` and `/api/registration-config`
|
|
||||||
- **Circuit breaker warnings** should appear if there are still issues
|
|
||||||
- **Proper initialization logging** from each page
|
|
||||||
|
|
||||||
### 3. Browser Developer Tools:
|
|
||||||
- **Network tab:** Should show minimal API calls
|
|
||||||
- **Console:** Should show clean initialization logs
|
|
||||||
- **Performance:** Reduced JavaScript execution on mobile
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
### New Files Created:
|
|
||||||
1. `utils/static-device-detection.ts` - Static device detection utility
|
|
||||||
2. `utils/config-cache.ts` - Global configuration cache with circuit breaker
|
|
||||||
3. `SAFARI_RELOAD_LOOP_FIX_COMPLETE.md` - This documentation
|
|
||||||
|
|
||||||
### Files Updated:
|
|
||||||
1. `pages/signup.vue` - Complete rewrite with reCAPTCHA v2 and static detection
|
|
||||||
2. `pages/auth/verify.vue` - Updated with static device detection
|
|
||||||
3. `pages/auth/setup-password.vue` - Updated with static device detection
|
|
||||||
|
|
||||||
## Monitoring and Maintenance
|
|
||||||
|
|
||||||
### Health Check:
|
|
||||||
- Monitor `/api/health` endpoint for system stability
|
|
||||||
- Check server logs for circuit breaker activations
|
|
||||||
- Monitor user registration completion rates
|
|
||||||
|
|
||||||
### Future Considerations:
|
|
||||||
- **reCAPTCHA v3 can be restored** once Safari iOS issues are resolved
|
|
||||||
- **Config cache can be extended** to other API endpoints if needed
|
|
||||||
- **Static device detection** can be used in other components
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
✅ **No reload loops** on Safari iOS for affected pages
|
|
||||||
✅ **Reduced API call frequency** (circuit breaker protection)
|
|
||||||
✅ **Maintained functionality** of all registration/verification flows
|
|
||||||
✅ **Improved performance** on mobile Safari
|
|
||||||
✅ **reCAPTCHA v2 integration** working properly
|
|
||||||
✅ **Proper error handling** and fallbacks in place
|
|
||||||
|
|
||||||
The implementation provides a robust, production-ready solution that eliminates the Safari iOS reload loops while maintaining all existing functionality and improving overall performance.
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -14,62 +14,8 @@ export default defineNuxtConfig({
|
||||||
console.log(`🌐 Server listening on http://${host}:${port}`)
|
console.log(`🌐 Server listening on http://${host}:${port}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modules: ["vuetify-nuxt-module",
|
modules: ["vuetify-nuxt-module"],
|
||||||
// TEMPORARILY DISABLED FOR TESTING - PWA causing reload loops on mobile Safari
|
css: [],
|
||||||
// [
|
|
||||||
// "@vite-pwa/nuxt",
|
|
||||||
// {
|
|
||||||
// registerType: 'autoUpdate',
|
|
||||||
// workbox: {
|
|
||||||
// globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
|
|
||||||
// navigateFallback: '/',
|
|
||||||
// navigateFallbackDenylist: [/^\/api\//]
|
|
||||||
// },
|
|
||||||
// client: {
|
|
||||||
// installPrompt: true,
|
|
||||||
// periodicSyncForUpdates: 20
|
|
||||||
// },
|
|
||||||
// devOptions: {
|
|
||||||
// enabled: true,
|
|
||||||
// suppressWarnings: true,
|
|
||||||
// navigateFallbackAllowlist: [/^\/$/],
|
|
||||||
// type: 'module'
|
|
||||||
// },
|
|
||||||
// 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',
|
|
||||||
// scope: '/',
|
|
||||||
// start_url: '/',
|
|
||||||
// icons: [
|
|
||||||
// {
|
|
||||||
// src: 'icon-192x192.png',
|
|
||||||
// sizes: '192x192',
|
|
||||||
// type: 'image/png'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// src: 'icon-512x512.png',
|
|
||||||
// sizes: '512x512',
|
|
||||||
// type: 'image/png'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// src: 'icon-512x512.png',
|
|
||||||
// sizes: '512x512',
|
|
||||||
// type: 'image/png',
|
|
||||||
// purpose: 'any maskable'
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
"@nuxtjs/device"],
|
|
||||||
css: [
|
|
||||||
'vuetify-pro-tiptap/style.css'
|
|
||||||
],
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
titleTemplate: "%s • MonacoUSA Portal",
|
titleTemplate: "%s • MonacoUSA Portal",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,14 +15,11 @@
|
||||||
"@fullcalendar/interaction": "^6.1.19",
|
"@fullcalendar/interaction": "^6.1.19",
|
||||||
"@fullcalendar/list": "^6.1.19",
|
"@fullcalendar/list": "^6.1.19",
|
||||||
"@fullcalendar/vue3": "^6.1.19",
|
"@fullcalendar/vue3": "^6.1.19",
|
||||||
"@nuxtjs/device": "^3.2.4",
|
|
||||||
"@types/handlebars": "^4.0.40",
|
"@types/handlebars": "^4.0.40",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@vite-pwa/nuxt": "^0.10.8",
|
"@vite-pwa/nuxt": "^0.10.8",
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"flag-icons": "^7.5.0",
|
|
||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
|
@ -32,12 +29,10 @@
|
||||||
"nodemailer": "^7.0.5",
|
"nodemailer": "^7.0.5",
|
||||||
"nuxt": "^3.15.4",
|
"nuxt": "^3.15.4",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
"systeminformation": "^5.27.7",
|
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-country-flag-next": "^2.3.2",
|
"vue-country-flag-next": "^2.3.2",
|
||||||
"vue-router": "latest",
|
"vue-router": "latest",
|
||||||
"vuetify-nuxt-module": "^0.18.3",
|
"vuetify-nuxt-module": "^0.18.3"
|
||||||
"vuetify-pro-tiptap": "^2.6.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
|
|
|
||||||
|
|
@ -179,13 +179,26 @@ definePageMeta({
|
||||||
middleware: 'guest'
|
middleware: 'guest'
|
||||||
});
|
});
|
||||||
|
|
||||||
import { getStaticDeviceInfo, getDeviceCssClasses, applyMobileSafariOptimizations, getMobileSafariViewportMeta } from '~/utils/static-device-detection';
|
// Device detection
|
||||||
|
const isMobile = ref(false);
|
||||||
|
const isMobileSafari = ref(false);
|
||||||
|
|
||||||
// Static device detection - no reactive dependencies
|
// Initialize device detection on mount
|
||||||
const deviceInfo = getStaticDeviceInfo();
|
onMounted(() => {
|
||||||
|
if (process.client) {
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
isMobile.value = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
||||||
|
isMobileSafari.value = /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Static CSS classes - computed once, never reactive
|
// CSS classes based on device detection
|
||||||
const containerClasses = ref(getDeviceCssClasses('password-setup-page'));
|
const containerClasses = computed(() => {
|
||||||
|
const classes = ['password-setup-page'];
|
||||||
|
if (isMobile.value) classes.push('is-mobile');
|
||||||
|
if (isMobileSafari.value) classes.push('is-mobile-safari', 'performance-mode');
|
||||||
|
return classes.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
@ -265,7 +278,7 @@ useHead({
|
||||||
name: 'description',
|
name: 'description',
|
||||||
content: 'Set your password to complete your MonacoUSA Portal registration.'
|
content: 'Set your password to complete your MonacoUSA Portal registration.'
|
||||||
},
|
},
|
||||||
{ name: 'viewport', content: getMobileSafariViewportMeta() }
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover' }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -333,24 +346,9 @@ const setupPassword = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Component initialization - Safari iOS reload loop prevention
|
// Component initialization
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log('[setup-password] Password setup page loaded for:', email.value);
|
console.log('[setup-password] Password setup page loaded for:', email.value);
|
||||||
|
|
||||||
// CRITICAL: Check reload loop prevention first
|
|
||||||
const { initReloadLoopPrevention } = await import('~/utils/reload-loop-prevention');
|
|
||||||
const canLoad = initReloadLoopPrevention('setup-password-page');
|
|
||||||
|
|
||||||
if (!canLoad) {
|
|
||||||
console.error('[setup-password] Page load blocked by reload loop prevention system');
|
|
||||||
return; // Stop all initialization if blocked
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply mobile Safari optimizations early
|
|
||||||
if (deviceInfo.isMobileSafari) {
|
|
||||||
applyMobileSafariOptimizations();
|
|
||||||
console.log('[setup-password] Mobile Safari optimizations applied');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have required parameters
|
// Check if we have required parameters
|
||||||
if (!email.value) {
|
if (!email.value) {
|
||||||
|
|
|
||||||
|
|
@ -91,26 +91,36 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getStaticDeviceInfo, getDeviceCssClasses, applyMobileSafariOptimizations, getMobileSafariViewportMeta } from '~/utils/static-device-detection';
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: false,
|
layout: false,
|
||||||
middleware: 'guest'
|
middleware: 'guest'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get query parameters - static to prevent reload loops
|
// Get query parameters
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const email = ref((route.query.email as string) || '');
|
const email = ref((route.query.email as string) || '');
|
||||||
const partialWarning = ref(route.query.warning === 'partial');
|
const partialWarning = ref(route.query.warning === 'partial');
|
||||||
|
|
||||||
// Static device detection - no reactive dependencies
|
// Simple device detection
|
||||||
const deviceInfo = getStaticDeviceInfo();
|
const isMobile = ref(false);
|
||||||
|
const isMobileSafari = ref(false);
|
||||||
|
|
||||||
// Static CSS classes - computed once, never reactive
|
// Initialize device detection on mount
|
||||||
const containerClasses = ref(getDeviceCssClasses('verification-success'));
|
onMounted(() => {
|
||||||
|
if (process.client) {
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
isMobile.value = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
||||||
|
isMobileSafari.value = /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Static setup password URL - no reactive dependencies
|
// CSS classes based on device detection
|
||||||
const setupPasswordUrl = 'https://auth.monacousa.org/realms/monacousa/account/';
|
const containerClasses = computed(() => {
|
||||||
|
const classes = ['verification-success'];
|
||||||
|
if (isMobile.value) classes.push('is-mobile');
|
||||||
|
if (isMobileSafari.value) classes.push('is-mobile-safari');
|
||||||
|
return classes.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
// Set page title with mobile viewport optimization
|
// Set page title with mobile viewport optimization
|
||||||
useHead({
|
useHead({
|
||||||
|
|
@ -120,7 +130,10 @@ useHead({
|
||||||
name: 'description',
|
name: 'description',
|
||||||
content: 'Your email has been successfully verified. You can now log in to the MonacoUSA Portal.'
|
content: 'Your email has been successfully verified. You can now log in to the MonacoUSA Portal.'
|
||||||
},
|
},
|
||||||
{ name: 'viewport', content: getMobileSafariViewportMeta() }
|
{
|
||||||
|
name: 'viewport',
|
||||||
|
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -134,19 +147,12 @@ const goToPasswordSetup = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track verification - Safari iOS reload loop prevention
|
// Track verification
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('[verify-success] Email verification completed', {
|
console.log('[verify-success] Email verification completed', {
|
||||||
email: email.value,
|
email: email.value,
|
||||||
partialWarning: partialWarning.value,
|
partialWarning: partialWarning.value
|
||||||
setupPasswordUrl: setupPasswordUrl
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply mobile Safari optimizations early
|
|
||||||
if (deviceInfo.isMobileSafari) {
|
|
||||||
applyMobileSafariOptimizations();
|
|
||||||
console.log('[verify-success] Mobile Safari optimizations applied');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,17 +54,14 @@
|
||||||
Verifying Your Email
|
Verifying Your Email
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="text-body-1 text-medium-emphasis" v-if="verificationState">
|
<p class="text-body-1 text-medium-emphasis">
|
||||||
{{ statusMessage || 'Please wait while we verify your email address...' }}
|
{{ statusMessage || 'Please wait while we verify your email address...' }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-body-1 text-medium-emphasis" v-else>
|
|
||||||
Please wait while we verify your email address...
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Attempt Counter -->
|
<!-- Attempt Counter -->
|
||||||
<div v-if="verificationState && verificationState.attempts > 1" class="mt-2">
|
<div v-if="attemptCount > 1" class="mt-2">
|
||||||
<v-chip size="small" color="primary" variant="outlined">
|
<v-chip size="small" color="primary" variant="outlined">
|
||||||
Attempt {{ verificationState.attempts }}/{{ verificationState.maxAttempts }}
|
Attempt {{ attemptCount }}/{{ maxAttempts }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -88,7 +85,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Circuit Breaker Status -->
|
<!-- Circuit Breaker Status -->
|
||||||
<div v-if="verificationState && statusMessage" class="mb-4">
|
<div v-if="statusMessage" class="mb-4">
|
||||||
<v-alert
|
<v-alert
|
||||||
type="info"
|
type="info"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
|
|
@ -210,18 +207,6 @@ definePageMeta({
|
||||||
middleware: 'guest'
|
middleware: 'guest'
|
||||||
});
|
});
|
||||||
|
|
||||||
import { getStaticDeviceInfo, getDeviceCssClasses, applyMobileSafariOptimizations, getMobileSafariViewportMeta } from '~/utils/static-device-detection';
|
|
||||||
import {
|
|
||||||
getVerificationState,
|
|
||||||
initVerificationState,
|
|
||||||
recordAttempt,
|
|
||||||
shouldBlockVerification,
|
|
||||||
getStatusMessage,
|
|
||||||
navigateWithFallback,
|
|
||||||
getMobileNavigationDelay,
|
|
||||||
type VerificationAttempt
|
|
||||||
} from '~/utils/verification-state';
|
|
||||||
|
|
||||||
// Get route and token immediately
|
// Get route and token immediately
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const token = route.query.token as string || '';
|
const token = route.query.token as string || '';
|
||||||
|
|
@ -231,17 +216,33 @@ const verifying = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const partialSuccess = ref(false);
|
const partialSuccess = ref(false);
|
||||||
|
|
||||||
// Verification state management
|
// Simple retry logic
|
||||||
const verificationState = ref<VerificationAttempt | null>(null);
|
|
||||||
const isBlocked = ref(false);
|
const isBlocked = ref(false);
|
||||||
const canRetry = ref(true);
|
const canRetry = ref(true);
|
||||||
const statusMessage = ref('');
|
const statusMessage = ref('');
|
||||||
|
const attemptCount = ref(0);
|
||||||
|
const maxAttempts = 3;
|
||||||
|
|
||||||
// Static device detection - no reactive dependencies
|
// Device detection
|
||||||
const deviceInfo = getStaticDeviceInfo();
|
const isMobile = ref(false);
|
||||||
|
const isMobileSafari = ref(false);
|
||||||
|
|
||||||
// Static container classes - must be reactive for template
|
// Initialize device detection on mount
|
||||||
const containerClasses = ref(getDeviceCssClasses('verification-page'));
|
onMounted(() => {
|
||||||
|
if (process.client) {
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
isMobile.value = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
||||||
|
isMobileSafari.value = /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// CSS classes based on device detection
|
||||||
|
const containerClasses = computed(() => {
|
||||||
|
const classes = ['verification-page'];
|
||||||
|
if (isMobile.value) classes.push('is-mobile');
|
||||||
|
if (isMobileSafari.value) classes.push('is-mobile-safari', 'performance-mode');
|
||||||
|
return classes.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
// Set page title with mobile viewport optimization
|
// Set page title with mobile viewport optimization
|
||||||
useHead({
|
useHead({
|
||||||
|
|
@ -251,49 +252,43 @@ useHead({
|
||||||
name: 'description',
|
name: 'description',
|
||||||
content: 'Verifying your email address for the MonacoUSA Portal.'
|
content: 'Verifying your email address for the MonacoUSA Portal.'
|
||||||
},
|
},
|
||||||
{ name: 'viewport', content: getMobileSafariViewportMeta() }
|
{
|
||||||
|
name: 'viewport',
|
||||||
|
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update UI state based on verification state
|
// Simple verification logic
|
||||||
const updateUIState = () => {
|
const updateUIState = () => {
|
||||||
if (!verificationState.value) return;
|
if (attemptCount.value >= maxAttempts) {
|
||||||
|
isBlocked.value = true;
|
||||||
statusMessage.value = getStatusMessage(verificationState.value);
|
canRetry.value = false;
|
||||||
isBlocked.value = shouldBlockVerification(token);
|
statusMessage.value = `Too many failed attempts. Please wait before trying again.`;
|
||||||
canRetry.value = verificationState.value.attempts < verificationState.value.maxAttempts && !isBlocked.value;
|
} else {
|
||||||
|
canRetry.value = attemptCount.value < maxAttempts;
|
||||||
console.log('[auth/verify] UI State updated:', {
|
}
|
||||||
status: verificationState.value.status,
|
|
||||||
attempts: verificationState.value.attempts,
|
|
||||||
isBlocked: isBlocked.value,
|
|
||||||
canRetry: canRetry.value
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify email function with circuit breaker
|
// Verify email function
|
||||||
const verifyEmail = async () => {
|
const verifyEmail = async () => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
error.value = 'No verification token provided. Please check your email for the correct verification link.';
|
error.value = 'No verification token provided. Please check your email for the correct verification link.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize or get existing verification state
|
if (attemptCount.value >= maxAttempts) {
|
||||||
verificationState.value = initVerificationState(token);
|
isBlocked.value = true;
|
||||||
updateUIState();
|
|
||||||
|
|
||||||
// Check if verification should be blocked
|
|
||||||
if (shouldBlockVerification(token)) {
|
|
||||||
console.log('[auth/verify] Verification blocked by circuit breaker');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[auth/verify] Starting verification attempt ${verificationState.value.attempts + 1}/${verificationState.value.maxAttempts}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
verifying.value = true;
|
verifying.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
partialSuccess.value = false;
|
partialSuccess.value = false;
|
||||||
|
attemptCount.value++;
|
||||||
|
|
||||||
|
console.log(`[auth/verify] Starting verification attempt ${attemptCount.value}/${maxAttempts}`);
|
||||||
|
|
||||||
// Call the API endpoint to verify the email
|
// Call the API endpoint to verify the email
|
||||||
const response = await $fetch(`/api/auth/verify-email?token=${token}`, {
|
const response = await $fetch(`/api/auth/verify-email?token=${token}`, {
|
||||||
|
|
@ -302,10 +297,6 @@ const verifyEmail = async () => {
|
||||||
|
|
||||||
console.log('[auth/verify] Email verification successful:', response);
|
console.log('[auth/verify] Email verification successful:', response);
|
||||||
|
|
||||||
// Record successful attempt
|
|
||||||
verificationState.value = recordAttempt(token, true);
|
|
||||||
updateUIState();
|
|
||||||
|
|
||||||
// Extract response data
|
// Extract response data
|
||||||
const email = response?.data?.email || '';
|
const email = response?.data?.email || '';
|
||||||
const isPartialSuccess = response?.data?.partialSuccess || false;
|
const isPartialSuccess = response?.data?.partialSuccess || false;
|
||||||
|
|
@ -335,26 +326,22 @@ const verifyEmail = async () => {
|
||||||
redirectUrl += '?' + queryParams.join('&');
|
redirectUrl += '?' + queryParams.join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use progressive navigation with mobile delay
|
// Navigate to success page
|
||||||
const navigationDelay = getMobileNavigationDelay();
|
console.log(`[auth/verify] Navigating to success page`);
|
||||||
console.log(`[auth/verify] Navigating to success page with ${navigationDelay}ms delay`);
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await navigateWithFallback(redirectUrl, { replace: true });
|
await navigateTo(redirectUrl, { replace: true });
|
||||||
} catch (navError) {
|
} catch (navError) {
|
||||||
console.error('[auth/verify] Navigation failed:', navError);
|
console.error('[auth/verify] Navigation failed:', navError);
|
||||||
// Final fallback - direct window location
|
// Final fallback - direct window location
|
||||||
window.location.replace(redirectUrl);
|
window.location.replace(redirectUrl);
|
||||||
}
|
}
|
||||||
}, navigationDelay);
|
}, 500);
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[auth/verify] Email verification failed:', err);
|
console.error('[auth/verify] Email verification failed:', err);
|
||||||
|
|
||||||
// Record failed attempt
|
|
||||||
const errorMessage = err.data?.message || err.message || 'Email verification failed';
|
|
||||||
verificationState.value = recordAttempt(token, false, errorMessage);
|
|
||||||
updateUIState();
|
updateUIState();
|
||||||
|
|
||||||
// Set error message based on status code
|
// Set error message based on status code
|
||||||
|
|
@ -367,7 +354,7 @@ const verifyEmail = async () => {
|
||||||
} else if (err.statusCode === 404) {
|
} else if (err.statusCode === 404) {
|
||||||
error.value = 'User not found. The verification token may be invalid.';
|
error.value = 'User not found. The verification token may be invalid.';
|
||||||
} else {
|
} else {
|
||||||
error.value = errorMessage;
|
error.value = err.data?.message || err.message || 'Email verification failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
verifying.value = false;
|
verifying.value = false;
|
||||||
|
|
@ -385,40 +372,15 @@ const retryVerification = async () => {
|
||||||
await verifyEmail();
|
await verifyEmail();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Component initialization - Safari iOS reload loop prevention
|
// Component initialization
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log('[auth/verify] Component mounted with token:', token?.substring(0, 20) + '...');
|
console.log('[auth/verify] Component mounted with token:', token?.substring(0, 20) + '...');
|
||||||
|
|
||||||
// CRITICAL: Check reload loop prevention first
|
|
||||||
const { initReloadLoopPrevention } = await import('~/utils/reload-loop-prevention');
|
|
||||||
const canLoad = initReloadLoopPrevention('verify-page');
|
|
||||||
|
|
||||||
if (!canLoad) {
|
|
||||||
console.error('[auth/verify] Page load blocked by reload loop prevention system');
|
|
||||||
return; // Stop all initialization if blocked
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply mobile Safari optimizations early
|
|
||||||
if (deviceInfo.isMobileSafari) {
|
|
||||||
applyMobileSafariOptimizations();
|
|
||||||
console.log('[auth/verify] Mobile Safari optimizations applied');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token exists
|
// Check if token exists
|
||||||
if (!token) {
|
if (!token) {
|
||||||
error.value = 'No verification token provided. Please check your email for the correct verification link.';
|
error.value = 'No verification token provided. Please check your email for the correct verification link.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize verification state
|
|
||||||
verificationState.value = initVerificationState(token, 3);
|
|
||||||
updateUIState();
|
|
||||||
|
|
||||||
// Check if verification is blocked before starting
|
|
||||||
if (shouldBlockVerification(token)) {
|
|
||||||
console.log('[auth/verify] Verification blocked by circuit breaker on mount');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start verification process with a small delay to ensure stability
|
// Start verification process with a small delay to ensure stability
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
||||||
|
|
@ -507,28 +507,64 @@ const deleteMember = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMemberCreated = (newMember: Member) => {
|
const handleMemberCreated = (newMember: Member) => {
|
||||||
|
console.log('[member-list] =====================================');
|
||||||
console.log('[member-list] handleMemberCreated called with:', JSON.stringify(newMember, null, 2));
|
console.log('[member-list] handleMemberCreated called with:', JSON.stringify(newMember, null, 2));
|
||||||
console.log('[member-list] newMember fields:', Object.keys(newMember));
|
console.log('[member-list] newMember fields:', Object.keys(newMember));
|
||||||
console.log('[member-list] FullName value:', `"${newMember.FullName}"`);
|
console.log('[member-list] FullName value:', `"${newMember.FullName}"`);
|
||||||
console.log('[member-list] first_name value:', `"${newMember.first_name}"`);
|
console.log('[member-list] first_name value:', `"${newMember.first_name}"`);
|
||||||
console.log('[member-list] last_name value:', `"${newMember.last_name}"`);
|
console.log('[member-list] last_name value:', `"${newMember.last_name}"`);
|
||||||
|
console.log('[member-list] nationality value:', `"${newMember.nationality}"`);
|
||||||
|
console.log('[member-list] email value:', `"${newMember.email}"`);
|
||||||
|
console.log('[member-list] member_id value:', `"${newMember.member_id}"`);
|
||||||
|
console.log('[member-list] membership_status value:', `"${newMember.membership_status}"`);
|
||||||
|
|
||||||
// Calculate FullName if it doesn't exist
|
// ADVANCED DEBUGGING: Check if data is actually missing
|
||||||
|
const hasFirstName = !!(newMember.first_name && newMember.first_name.trim());
|
||||||
|
const hasLastName = !!(newMember.last_name && newMember.last_name.trim());
|
||||||
|
const hasFullName = !!(newMember.FullName && newMember.FullName.trim());
|
||||||
|
|
||||||
|
console.log('[member-list] Data validation:');
|
||||||
|
console.log(' - hasFirstName:', hasFirstName);
|
||||||
|
console.log(' - hasLastName:', hasLastName);
|
||||||
|
console.log(' - hasFullName:', hasFullName);
|
||||||
|
|
||||||
|
// If the API response is missing data, refresh the entire member list instead
|
||||||
|
if (!hasFirstName || !hasLastName || !hasFullName) {
|
||||||
|
console.error('[member-list] ❌ API response missing critical member data, refreshing member list...');
|
||||||
|
loadMembers();
|
||||||
|
showSuccess.value = true;
|
||||||
|
successMessage.value = 'Member created successfully. Refreshing member list...';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate FullName with robust fallback
|
||||||
const fullName = newMember.FullName ||
|
const fullName = newMember.FullName ||
|
||||||
`${newMember.first_name || ''} ${newMember.last_name || ''}`.trim() ||
|
`${newMember.first_name || ''} ${newMember.last_name || ''}`.trim() ||
|
||||||
'New Member';
|
'New Member';
|
||||||
|
|
||||||
console.log('[member-list] Calculated FullName:', `"${fullName}"`);
|
console.log('[member-list] Calculated FullName:', `"${fullName}"`);
|
||||||
|
|
||||||
// Ensure the member has a FullName for display
|
// Ensure the member has complete data for display
|
||||||
const memberWithFullName = {
|
const memberWithCompleteData = {
|
||||||
...newMember,
|
...newMember,
|
||||||
FullName: fullName
|
FullName: fullName,
|
||||||
|
// Ensure all required fields are present
|
||||||
|
first_name: newMember.first_name || '',
|
||||||
|
last_name: newMember.last_name || '',
|
||||||
|
nationality: newMember.nationality || '',
|
||||||
|
email: newMember.email || '',
|
||||||
|
membership_status: newMember.membership_status || 'Active'
|
||||||
};
|
};
|
||||||
|
|
||||||
members.value.unshift(memberWithFullName);
|
console.log('[member-list] Final member data:', JSON.stringify(memberWithCompleteData, null, 2));
|
||||||
|
console.log('[member-list] Adding member to beginning of list...');
|
||||||
|
|
||||||
|
members.value.unshift(memberWithCompleteData);
|
||||||
showSuccess.value = true;
|
showSuccess.value = true;
|
||||||
successMessage.value = `${fullName} has been added successfully.`;
|
successMessage.value = `${fullName} has been added successfully.`;
|
||||||
|
|
||||||
|
console.log('[member-list] ✅ Member added to local list, total count:', members.value.length);
|
||||||
|
console.log('[member-list] =====================================');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMemberUpdated = (updatedMember: Member) => {
|
const handleMemberUpdated = (updatedMember: Member) => {
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,70 @@
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Profile Photo -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-account-circle</v-icon>
|
||||||
|
Profile Photo
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div class="d-flex align-center flex-wrap">
|
||||||
|
<!-- Avatar Preview -->
|
||||||
|
<div class="mr-6 mb-4 text-center">
|
||||||
|
<ProfileAvatar
|
||||||
|
v-if="memberData"
|
||||||
|
:member-id="memberData.member_id"
|
||||||
|
:member-name="fullName"
|
||||||
|
:first-name="memberData.first_name"
|
||||||
|
:last-name="memberData.last_name"
|
||||||
|
size="large"
|
||||||
|
:key="avatarBustKey"
|
||||||
|
class="mb-2"
|
||||||
|
/>
|
||||||
|
<p class="text-body-2 text-medium-emphasis">Current Photo</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Controls -->
|
||||||
|
<div class="flex-grow-1 mb-4">
|
||||||
|
<v-file-input
|
||||||
|
v-model="selectedFiles"
|
||||||
|
accept="image/jpeg,image/png,image/webp"
|
||||||
|
label="Choose new profile photo (uploads automatically)"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
prepend-icon="mdi-camera"
|
||||||
|
show-size
|
||||||
|
:disabled="uploading || deleting"
|
||||||
|
:loading="uploading"
|
||||||
|
@update:model-value="onSelectImage"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-delete"
|
||||||
|
:loading="deleting"
|
||||||
|
:disabled="uploading || !memberData?.member_id"
|
||||||
|
@click="confirmDelete = true"
|
||||||
|
>
|
||||||
|
Remove Photo
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-body-2 text-medium-emphasis mt-2">
|
||||||
|
Supported formats: JPG, PNG, WEBP • Maximum size: 5MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<!-- Profile Information -->
|
<!-- Profile Information -->
|
||||||
<v-row>
|
<v-row>
|
||||||
<!-- Personal Information -->
|
<!-- Personal Information -->
|
||||||
|
|
@ -295,6 +359,35 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<v-dialog v-model="confirmDelete" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Remove Profile Photo?
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Are you sure you want to remove your profile photo? This action cannot be undone.
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
@click="confirmDelete = false"
|
||||||
|
:disabled="deleting"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:loading="deleting"
|
||||||
|
@click="confirmDeleteImage"
|
||||||
|
>
|
||||||
|
Remove Photo
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -310,13 +403,18 @@ const { user, userTier } = useAuth();
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const memberData = ref<Member | null>(null);
|
|
||||||
const snackbar = ref({
|
const snackbar = ref({
|
||||||
show: false,
|
show: false,
|
||||||
message: '',
|
message: '',
|
||||||
color: 'success'
|
color: 'success'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch complete member data (same as user.vue)
|
||||||
|
const { data: sessionData, pending: sessionPending, error: sessionError, refresh: refreshSession } =
|
||||||
|
await useFetch<{ success: boolean; member: Member }>('/api/auth/session', { server: false });
|
||||||
|
|
||||||
|
const memberData = computed<Member | null>(() => sessionData.value?.member || null);
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const fullName = computed(() => {
|
const fullName = computed(() => {
|
||||||
if (memberData.value) {
|
if (memberData.value) {
|
||||||
|
|
@ -336,19 +434,20 @@ const daysRemaining = computed(() => {
|
||||||
return diffDays;
|
return diffDays;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Profile image state
|
||||||
|
const uploading = ref(false);
|
||||||
|
const deleting = ref(false);
|
||||||
|
const avatarBustKey = ref(0);
|
||||||
|
const selectedFiles = ref<File[]>([]);
|
||||||
|
const confirmDelete = ref(false);
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const loadMemberData = async () => {
|
const loadMemberData = async () => {
|
||||||
if (!user.value?.email) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const response = await $fetch('/api/members') as any;
|
await refreshSession();
|
||||||
const members = response?.data || response?.list || [];
|
if (!sessionData.value?.member) {
|
||||||
|
throw new Error('Missing member in session');
|
||||||
// Find member by email
|
|
||||||
const member = members.find((m: any) => m.email === user.value?.email);
|
|
||||||
if (member) {
|
|
||||||
memberData.value = member;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load member data:', error);
|
console.error('Failed to load member data:', error);
|
||||||
|
|
@ -362,6 +461,65 @@ const loadMemberData = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Profile image helpers
|
||||||
|
const onSelectImage = async (files: File[] | File | null) => {
|
||||||
|
const fileList = Array.isArray(files) ? files : files ? [files] : [];
|
||||||
|
if (fileList.length === 0) return;
|
||||||
|
const file = fileList[0];
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
const maxBytes = 5 * 1024 * 1024; // 5MB
|
||||||
|
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
|
||||||
|
if (!allowed.includes(file.type)) {
|
||||||
|
snackbar.value = { show: true, message: 'Only JPG, PNG or WEBP images are allowed.', color: 'error' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > maxBytes) {
|
||||||
|
snackbar.value = { show: true, message: 'Image must be 5MB or smaller.', color: 'error' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
uploading.value = true;
|
||||||
|
const body = new FormData();
|
||||||
|
body.append('file', file);
|
||||||
|
await $fetch('/api/profile/upload-image', {
|
||||||
|
method: 'POST',
|
||||||
|
body
|
||||||
|
});
|
||||||
|
avatarBustKey.value++;
|
||||||
|
selectedFiles.value = []; // Clear the file input
|
||||||
|
snackbar.value = { show: true, message: 'Profile image updated.', color: 'success' };
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
snackbar.value = { show: true, message: 'Failed to upload image.', color: 'error' };
|
||||||
|
} finally {
|
||||||
|
uploading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDeleteImage = async () => {
|
||||||
|
confirmDelete.value = false;
|
||||||
|
await onDeleteImage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteImage = async () => {
|
||||||
|
if (!memberData.value?.member_id) return;
|
||||||
|
try {
|
||||||
|
deleting.value = true;
|
||||||
|
await $fetch(`/api/profile/image/${encodeURIComponent(memberData.value.member_id)}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
avatarBustKey.value++;
|
||||||
|
snackbar.value = { show: true, message: 'Profile image removed.', color: 'success' };
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
snackbar.value = { show: true, message: 'Failed to delete image.', color: 'error' };
|
||||||
|
} finally {
|
||||||
|
deleting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const copyMemberID = async () => {
|
const copyMemberID = async () => {
|
||||||
if (!memberData.value?.member_id) return;
|
if (!memberData.value?.member_id) return;
|
||||||
|
|
||||||
|
|
@ -431,6 +589,11 @@ onMounted(() => {
|
||||||
loadMemberData();
|
loadMemberData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watch for session loading
|
||||||
|
watch(sessionPending, (isPending) => {
|
||||||
|
loading.value = isPending;
|
||||||
|
});
|
||||||
|
|
||||||
// Watch for user changes
|
// Watch for user changes
|
||||||
watch(user, () => {
|
watch(user, () => {
|
||||||
if (user.value) {
|
if (user.value) {
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,6 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { RegistrationFormData } from '~/utils/types';
|
import type { RegistrationFormData } from '~/utils/types';
|
||||||
import { getStaticDeviceInfo, getDeviceCssClasses, applyMobileSafariOptimizations, getMobileSafariViewportMeta } from '~/utils/static-device-detection';
|
|
||||||
import { loadAllConfigs } from '~/utils/config-cache';
|
import { loadAllConfigs } from '~/utils/config-cache';
|
||||||
|
|
||||||
// Page metadata
|
// Page metadata
|
||||||
|
|
@ -219,28 +218,47 @@ definePageMeta({
|
||||||
layout: false
|
layout: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Static device detection - no reactive dependencies
|
// Device detection
|
||||||
const deviceInfo = getStaticDeviceInfo();
|
const isMobile = ref(false);
|
||||||
|
const isMobileSafari = ref(false);
|
||||||
|
|
||||||
// Head configuration with static device-optimized viewport
|
// Initialize device detection on mount
|
||||||
|
onMounted(() => {
|
||||||
|
if (process.client) {
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
isMobile.value = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
||||||
|
isMobileSafari.value = /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Head configuration with mobile-optimized viewport
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Register - MonacoUSA Portal',
|
title: 'Register - MonacoUSA Portal',
|
||||||
meta: [
|
meta: [
|
||||||
{ name: 'description', content: 'Register to become a member of MonacoUSA Association' },
|
{ name: 'description', content: 'Register to become a member of MonacoUSA Association' },
|
||||||
{ name: 'robots', content: 'noindex, nofollow' },
|
{ name: 'robots', content: 'noindex, nofollow' },
|
||||||
{ name: 'viewport', content: getMobileSafariViewportMeta() }
|
{
|
||||||
|
name: 'viewport',
|
||||||
|
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Static CSS classes - computed once, never reactive
|
// CSS classes based on device detection
|
||||||
const containerClasses = ref(getDeviceCssClasses('signup-container'));
|
const containerClasses = computed(() => {
|
||||||
const cardClasses = ref((() => {
|
const classes = ['signup-container'];
|
||||||
|
if (isMobile.value) classes.push('is-mobile');
|
||||||
|
if (isMobileSafari.value) classes.push('is-mobile-safari', 'performance-optimized');
|
||||||
|
return classes.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
|
const cardClasses = computed(() => {
|
||||||
const classes = ['signup-card'];
|
const classes = ['signup-card'];
|
||||||
if (deviceInfo.isMobileSafari) {
|
if (isMobileSafari.value) {
|
||||||
classes.push('performance-optimized', 'no-backdrop-filter');
|
classes.push('performance-optimized', 'no-backdrop-filter');
|
||||||
}
|
}
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
})()); // Execute immediately and store the result, not the function
|
});
|
||||||
|
|
||||||
// Form data - individual refs to prevent Vue reactivity corruption
|
// Form data - individual refs to prevent Vue reactivity corruption
|
||||||
const firstName = ref('');
|
const firstName = ref('');
|
||||||
|
|
@ -426,30 +444,13 @@ const goToLogin = () => {
|
||||||
// Flag to prevent multiple initialization calls
|
// Flag to prevent multiple initialization calls
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
// Component initialization - Safari iOS reload loop prevention
|
// Component initialization
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Prevent multiple initializations
|
// Prevent multiple initializations
|
||||||
if (initialized || typeof window === 'undefined') return;
|
if (initialized || typeof window === 'undefined') return;
|
||||||
|
|
||||||
console.log('[signup] Starting signup page initialization...');
|
|
||||||
|
|
||||||
// CRITICAL: Check reload loop prevention first
|
|
||||||
const { initReloadLoopPrevention } = await import('~/utils/reload-loop-prevention');
|
|
||||||
const canLoad = initReloadLoopPrevention('signup-page');
|
|
||||||
|
|
||||||
if (!canLoad) {
|
|
||||||
console.error('[signup] Page load blocked by reload loop prevention system');
|
|
||||||
return; // Stop all initialization if blocked
|
|
||||||
}
|
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
// Apply mobile Safari optimizations early
|
|
||||||
if (deviceInfo.isMobileSafari) {
|
|
||||||
applyMobileSafariOptimizations();
|
|
||||||
console.log('[signup] Mobile Safari optimizations applied');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up reCAPTCHA callbacks before loading configs
|
// Set up reCAPTCHA callbacks before loading configs
|
||||||
setupRecaptchaCallbacks();
|
setupRecaptchaCallbacks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,25 @@
|
||||||
/**
|
/**
|
||||||
* Config Cache Initialization Plugin with Advanced Reload Loop Prevention
|
* Config Cache Initialization Plugin
|
||||||
* Comprehensive mobile Safari reload loop prevention system
|
* Simplified config cache initialization
|
||||||
* Integrates with reload-loop-prevention utility for maximum protection
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { initReloadLoopPrevention, applyMobileSafariReloadLoopFixes } from '~/utils/reload-loop-prevention';
|
|
||||||
|
|
||||||
export default defineNuxtPlugin({
|
export default defineNuxtPlugin({
|
||||||
name: 'config-cache-init',
|
name: 'config-cache-init',
|
||||||
enforce: 'pre', // Run before other plugins
|
enforce: 'pre', // Run before other plugins
|
||||||
async setup() {
|
async setup() {
|
||||||
console.log('[config-cache-init] Initializing comprehensive config cache and reload prevention plugin');
|
|
||||||
|
|
||||||
// Only run on client side
|
// Only run on client side
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emergency reload loop prevention - check immediately
|
|
||||||
const canLoad = initReloadLoopPrevention('config-cache-init');
|
|
||||||
if (!canLoad) {
|
|
||||||
console.error('[config-cache-init] Plugin load blocked by reload loop prevention');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize a flag to prevent multiple initializations
|
// Initialize a flag to prevent multiple initializations
|
||||||
if ((window as any).__configCacheInitialized) {
|
if ((window as any).__configCacheInitialized) {
|
||||||
console.log('[config-cache-init] Config cache already initialized');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as initialized
|
// Mark as initialized
|
||||||
(window as any).__configCacheInitialized = true;
|
(window as any).__configCacheInitialized = true;
|
||||||
|
|
||||||
// Apply mobile Safari specific fixes first
|
|
||||||
applyMobileSafariReloadLoopFixes();
|
|
||||||
|
|
||||||
// Initialize the config cache structure if not already present
|
// Initialize the config cache structure if not already present
|
||||||
if (!(window as any).__configCache) {
|
if (!(window as any).__configCache) {
|
||||||
(window as any).__configCache = {
|
(window as any).__configCache = {
|
||||||
|
|
@ -46,150 +30,11 @@ export default defineNuxtPlugin({
|
||||||
recaptchaError: null,
|
recaptchaError: null,
|
||||||
registrationError: null
|
registrationError: null
|
||||||
};
|
};
|
||||||
console.log('[config-cache-init] Config cache structure initialized');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize call history for circuit breaker
|
// Initialize call history for circuit breaker
|
||||||
if (!(window as any).__configCallHistory) {
|
if (!(window as any).__configCallHistory) {
|
||||||
(window as any).__configCallHistory = {};
|
(window as any).__configCallHistory = {};
|
||||||
console.log('[config-cache-init] Call history initialized');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced error handler with reload loop detection
|
|
||||||
const originalError = window.onerror;
|
|
||||||
window.onerror = function(msg, url, lineNo, columnNo, error) {
|
|
||||||
// Check for common reload loop patterns
|
|
||||||
if (typeof msg === 'string') {
|
|
||||||
const isReloadLoop = (
|
|
||||||
msg.includes('Maximum call stack') ||
|
|
||||||
msg.includes('too much recursion') ||
|
|
||||||
msg.includes('RangeError') ||
|
|
||||||
msg.includes('Script error') ||
|
|
||||||
msg.includes('ResizeObserver loop limit exceeded') ||
|
|
||||||
msg.includes('Non-Error promise rejection captured')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isReloadLoop) {
|
|
||||||
console.error('[config-cache-init] Potential reload loop detected:', {
|
|
||||||
message: msg,
|
|
||||||
url,
|
|
||||||
lineNo,
|
|
||||||
columnNo,
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Record this as a potential reload trigger
|
|
||||||
initReloadLoopPrevention('error-handler');
|
|
||||||
|
|
||||||
// Prevent default error handling which might cause reload
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for config-related errors
|
|
||||||
if (msg.includes('config') || msg.includes('fetch') || msg.includes('load')) {
|
|
||||||
console.warn('[config-cache-init] Config-related error detected:', msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call original error handler if it exists
|
|
||||||
if (originalError) {
|
|
||||||
return originalError(msg, url, lineNo, columnNo, error);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enhanced unhandled rejection handler
|
|
||||||
window.addEventListener('unhandledrejection', (event) => {
|
|
||||||
const reason = event.reason;
|
|
||||||
const isConfigRelated = (
|
|
||||||
reason?.message?.includes('config') ||
|
|
||||||
reason?.message?.includes('reload') ||
|
|
||||||
reason?.message?.includes('fetch') ||
|
|
||||||
reason?.message?.includes('/api/')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isConfigRelated) {
|
|
||||||
console.error('[config-cache-init] Unhandled config-related rejection:', {
|
|
||||||
reason,
|
|
||||||
stack: reason?.stack,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Record potential reload trigger
|
|
||||||
initReloadLoopPrevention('promise-rejection');
|
|
||||||
|
|
||||||
// Prevent default which might cause reload
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add performance monitoring for mobile Safari
|
|
||||||
if ('performance' in window && 'navigation' in window.performance) {
|
|
||||||
const navTiming = performance.navigation;
|
|
||||||
if (navTiming.type === 1) { // TYPE_RELOAD
|
|
||||||
console.warn('[config-cache-init] Page was reloaded - checking for reload loops');
|
|
||||||
const canContinue = initReloadLoopPrevention('page-reload');
|
|
||||||
if (!canContinue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monitor for rapid API calls that could indicate loops
|
|
||||||
const originalFetch = window.fetch;
|
|
||||||
let apiCallCount = 0;
|
|
||||||
let apiCallWindow = Date.now();
|
|
||||||
|
|
||||||
window.fetch = function(input: RequestInfo | URL, init?: RequestInit) {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Reset counter every 5 seconds
|
|
||||||
if (now - apiCallWindow > 5000) {
|
|
||||||
apiCallCount = 0;
|
|
||||||
apiCallWindow = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
apiCallCount++;
|
|
||||||
|
|
||||||
// Check for excessive API calls
|
|
||||||
if (apiCallCount > 10) {
|
|
||||||
const url = typeof input === 'string' ? input : input.toString();
|
|
||||||
console.warn(`[config-cache-init] Excessive API calls detected: ${apiCallCount} calls in 5s`, url);
|
|
||||||
|
|
||||||
// Check if this could be a config-related loop
|
|
||||||
if (url.includes('/api/recaptcha-config') || url.includes('/api/registration-config')) {
|
|
||||||
console.error('[config-cache-init] Config API loop detected!', url);
|
|
||||||
initReloadLoopPrevention('config-api-loop');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalFetch.call(this, input, init);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add visibility change handler for mobile Safari
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (!document.hidden) {
|
|
||||||
// Page became visible - might be returning from background
|
|
||||||
console.log('[config-cache-init] Page visibility restored');
|
|
||||||
setTimeout(() => {
|
|
||||||
// Verify cache is still intact after visibility change
|
|
||||||
const cache = (window as any).__configCache;
|
|
||||||
if (!cache) {
|
|
||||||
console.warn('[config-cache-init] Config cache lost after visibility change - reinitializing');
|
|
||||||
(window as any).__configCache = {
|
|
||||||
recaptcha: null,
|
|
||||||
registration: null,
|
|
||||||
recaptchaLoading: false,
|
|
||||||
registrationLoading: false,
|
|
||||||
recaptchaError: null,
|
|
||||||
registrationError: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[config-cache-init] Comprehensive config cache and reload prevention plugin initialized successfully');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { VuetifyTiptap, VuetifyViewer, createVuetifyProTipTap } from 'vuetify-pro-tiptap'
|
|
||||||
import 'vuetify-pro-tiptap/style.css'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
nuxtApp.vueApp.use(createVuetifyProTipTap({
|
|
||||||
lang: 'en',
|
|
||||||
theme: 'light'
|
|
||||||
}))
|
|
||||||
|
|
||||||
nuxtApp.vueApp.component('VuetifyTiptap', VuetifyTiptap)
|
|
||||||
nuxtApp.vueApp.component('VuetifyViewer', VuetifyViewer)
|
|
||||||
})
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
// server/api/admin/backfill-event-ids.post.ts
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
console.log('[admin/backfill-event-ids] Starting event_id backfill process...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Verify admin access
|
|
||||||
const sessionManager = createSessionManager();
|
|
||||||
const cookieHeader = getHeader(event, 'cookie');
|
|
||||||
const session = sessionManager.getSession(cookieHeader);
|
|
||||||
|
|
||||||
if (!session || session.user.tier !== 'admin') {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 403,
|
|
||||||
statusMessage: 'Admin access required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[admin/backfill-event-ids] Admin access verified for user: ${session.user.email}`);
|
|
||||||
|
|
||||||
const { createNocoDBEventsClient } = await import('~/server/utils/nocodb-events');
|
|
||||||
const eventsClient = createNocoDBEventsClient();
|
|
||||||
|
|
||||||
// Get all events
|
|
||||||
const response = await eventsClient.findAll({ limit: 1000 });
|
|
||||||
const events = response.list || [];
|
|
||||||
|
|
||||||
console.log(`[admin/backfill-event-ids] Found ${events.length} events to process`);
|
|
||||||
|
|
||||||
const results = {
|
|
||||||
processed: 0,
|
|
||||||
updated: 0,
|
|
||||||
skipped: 0,
|
|
||||||
errors: 0,
|
|
||||||
details: [] as any[]
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const eventItem of events) {
|
|
||||||
results.processed++;
|
|
||||||
const eventId = (eventItem as any).Id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if event_id already exists
|
|
||||||
if (eventItem.event_id && eventItem.event_id.trim() !== '') {
|
|
||||||
console.log(`[admin/backfill-event-ids] Event ${eventId} already has event_id: ${eventItem.event_id}`);
|
|
||||||
results.skipped++;
|
|
||||||
results.details.push({
|
|
||||||
id: eventId,
|
|
||||||
title: eventItem.title,
|
|
||||||
status: 'skipped',
|
|
||||||
reason: 'Already has event_id',
|
|
||||||
existing_event_id: eventItem.event_id
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate unique event_id based on event date and title
|
|
||||||
const eventDate = new Date(eventItem.start_datetime);
|
|
||||||
const dateString = eventDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD
|
|
||||||
const timeString = eventDate.toISOString().split('T')[1].split(':').slice(0,2).join(''); // HHMM
|
|
||||||
const titleSlug = eventItem.title.toLowerCase().replace(/[^a-z0-9]/g, '').substring(0, 8);
|
|
||||||
|
|
||||||
const newEventId = `evt_${dateString}_${timeString}_${titleSlug}`;
|
|
||||||
|
|
||||||
console.log(`[admin/backfill-event-ids] Updating event ${eventId} (${eventItem.title}) with event_id: ${newEventId}`);
|
|
||||||
|
|
||||||
// Update the event with the new event_id
|
|
||||||
await eventsClient.update(eventId.toString(), { event_id: newEventId });
|
|
||||||
|
|
||||||
results.updated++;
|
|
||||||
results.details.push({
|
|
||||||
id: eventId,
|
|
||||||
title: eventItem.title,
|
|
||||||
status: 'updated',
|
|
||||||
new_event_id: newEventId
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (updateError: any) {
|
|
||||||
console.error(`[admin/backfill-event-ids] Error updating event ${eventId}:`, updateError);
|
|
||||||
results.errors++;
|
|
||||||
results.details.push({
|
|
||||||
id: eventId,
|
|
||||||
title: eventItem.title,
|
|
||||||
status: 'error',
|
|
||||||
error: updateError.message || 'Update failed'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[admin/backfill-event-ids] Backfill completed. Processed: ${results.processed}, Updated: ${results.updated}, Skipped: ${results.skipped}, Errors: ${results.errors}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `Event ID backfill completed successfully`,
|
|
||||||
data: results
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[admin/backfill-event-ids] Backfill process failed:', error);
|
|
||||||
throw createError({
|
|
||||||
statusCode: 500,
|
|
||||||
statusMessage: error.message || 'Event ID backfill failed'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
import type { Member } from '~/utils/types';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] =========================');
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] POST /api/admin/cleanup-accounts - Account cleanup for expired members');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate session and require admin privileges
|
|
||||||
const sessionManager = createSessionManager();
|
|
||||||
const cookieHeader = getCookie(event, 'monacousa-session') ? getHeader(event, 'cookie') : undefined;
|
|
||||||
const session = sessionManager.getSession(cookieHeader);
|
|
||||||
|
|
||||||
if (!session?.user) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 401,
|
|
||||||
statusMessage: 'Authentication required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Require admin privileges for account cleanup
|
|
||||||
if (session.user.tier !== 'admin') {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 403,
|
|
||||||
statusMessage: 'Admin privileges required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] Authorized admin:', session.user.email);
|
|
||||||
|
|
||||||
// Get cleanup options from request body (optional)
|
|
||||||
const body = await readBody(event).catch(() => ({}));
|
|
||||||
const dryRun = body?.dryRun === true;
|
|
||||||
const monthsOverdue = body?.monthsOverdue || 3;
|
|
||||||
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] Cleanup options:', { dryRun, monthsOverdue });
|
|
||||||
|
|
||||||
// Calculate cutoff date (default: 3 months ago)
|
|
||||||
const cutoffDate = new Date();
|
|
||||||
cutoffDate.setMonth(cutoffDate.getMonth() - monthsOverdue);
|
|
||||||
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] Cutoff date:', cutoffDate.toISOString());
|
|
||||||
|
|
||||||
// Find members registered before cutoff date with unpaid dues
|
|
||||||
const { getMembers } = await import('~/server/utils/nocodb');
|
|
||||||
const membersResult = await getMembers();
|
|
||||||
const allMembers = membersResult.list || [];
|
|
||||||
|
|
||||||
const expiredMembers = allMembers.filter((member: Member) => {
|
|
||||||
// Must have a registration date
|
|
||||||
if (!member.registration_date) return false;
|
|
||||||
|
|
||||||
// Must be registered before cutoff date
|
|
||||||
const registrationDate = new Date(member.registration_date);
|
|
||||||
if (registrationDate >= cutoffDate) return false;
|
|
||||||
|
|
||||||
// Must have unpaid dues
|
|
||||||
if (member.current_year_dues_paid === 'true') return false;
|
|
||||||
|
|
||||||
// Must have a Keycloak ID (portal account)
|
|
||||||
if (!member.keycloak_id) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] Found', expiredMembers.length, 'expired members for cleanup');
|
|
||||||
|
|
||||||
const deletedAccounts = [];
|
|
||||||
const failedDeletions = [];
|
|
||||||
|
|
||||||
if (!dryRun && expiredMembers.length > 0) {
|
|
||||||
const { createKeycloakAdminClient } = await import('~/server/utils/keycloak-admin');
|
|
||||||
const { deleteMember } = await import('~/server/utils/nocodb');
|
|
||||||
const keycloakAdmin = createKeycloakAdminClient();
|
|
||||||
|
|
||||||
for (const member of expiredMembers) {
|
|
||||||
try {
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] Processing cleanup for:', member.email);
|
|
||||||
|
|
||||||
// Delete from Keycloak first
|
|
||||||
if (member.keycloak_id) {
|
|
||||||
try {
|
|
||||||
await keycloakAdmin.deleteUser(member.keycloak_id);
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] Deleted Keycloak user:', member.keycloak_id);
|
|
||||||
} catch (keycloakError: any) {
|
|
||||||
console.warn('[api/admin/cleanup-accounts.post] Failed to delete Keycloak user:', keycloakError.message);
|
|
||||||
// Continue with member deletion even if Keycloak deletion fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete member record
|
|
||||||
await deleteMember(member.Id);
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] Deleted member record:', member.Id);
|
|
||||||
|
|
||||||
deletedAccounts.push({
|
|
||||||
id: member.Id,
|
|
||||||
email: member.email,
|
|
||||||
name: `${member.first_name} ${member.last_name}`,
|
|
||||||
registrationDate: member.registration_date,
|
|
||||||
keycloakId: member.keycloak_id
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[api/admin/cleanup-accounts.post] Failed to delete account for', member.email, ':', error);
|
|
||||||
failedDeletions.push({
|
|
||||||
id: member.Id,
|
|
||||||
email: member.email,
|
|
||||||
name: `${member.first_name} ${member.last_name}`,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
success: true,
|
|
||||||
dryRun,
|
|
||||||
monthsOverdue,
|
|
||||||
cutoffDate: cutoffDate.toISOString(),
|
|
||||||
totalExpiredMembers: expiredMembers.length,
|
|
||||||
deletedCount: deletedAccounts.length,
|
|
||||||
failedCount: failedDeletions.length,
|
|
||||||
message: dryRun
|
|
||||||
? `Found ${expiredMembers.length} expired accounts that would be deleted (dry run)`
|
|
||||||
: `Cleaned up ${deletedAccounts.length} expired accounts${failedDeletions.length > 0 ? ` (${failedDeletions.length} failed)` : ''}`,
|
|
||||||
data: {
|
|
||||||
expiredMembers: expiredMembers.map(m => ({
|
|
||||||
id: m.Id,
|
|
||||||
email: m.email,
|
|
||||||
name: `${m.first_name} ${m.last_name}`,
|
|
||||||
registrationDate: m.registration_date,
|
|
||||||
daysSinceRegistration: Math.floor((Date.now() - new Date(m.registration_date || '').getTime()) / (1000 * 60 * 60 * 24)),
|
|
||||||
hasKeycloakAccount: !!m.keycloak_id
|
|
||||||
})),
|
|
||||||
deleted: deletedAccounts,
|
|
||||||
failed: failedDeletions
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] ✅ Account cleanup completed');
|
|
||||||
console.log('[api/admin/cleanup-accounts.post] Summary:', {
|
|
||||||
found: expiredMembers.length,
|
|
||||||
deleted: deletedAccounts.length,
|
|
||||||
failed: failedDeletions.length,
|
|
||||||
dryRun
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[api/admin/cleanup-accounts.post] ❌ Account cleanup failed:', error);
|
|
||||||
|
|
||||||
// If it's already an HTTP error, re-throw it
|
|
||||||
if (error.statusCode) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, wrap it in a generic error
|
|
||||||
throw createError({
|
|
||||||
statusCode: 500,
|
|
||||||
statusMessage: error.message || 'Account cleanup failed'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
// server/api/admin/debug-rsvp-config.get.ts
|
|
||||||
import { createSessionManager } from '~/server/utils/session';
|
|
||||||
import { getEffectiveNocoDBConfig } from '~/server/utils/admin-config';
|
|
||||||
import { getEventTableId } from '~/server/utils/nocodb-events';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
try {
|
|
||||||
// Verify admin session
|
|
||||||
const sessionManager = createSessionManager();
|
|
||||||
const cookieHeader = getHeader(event, 'cookie');
|
|
||||||
const session = sessionManager.getSession(cookieHeader);
|
|
||||||
|
|
||||||
if (!session?.user || session.user.tier !== 'admin') {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 403,
|
|
||||||
statusMessage: 'Admin access required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[admin/debug-rsvp-config] =========================');
|
|
||||||
console.log('[admin/debug-rsvp-config] 🔍 DEBUG: RSVP Configuration Analysis');
|
|
||||||
|
|
||||||
// Get effective config
|
|
||||||
const effectiveConfig = getEffectiveNocoDBConfig();
|
|
||||||
console.log('[admin/debug-rsvp-config] 🔍 Effective config:', JSON.stringify(effectiveConfig, null, 2));
|
|
||||||
|
|
||||||
// Test RSVP table ID retrieval
|
|
||||||
const rsvpTableId = getEventTableId('EventRSVPs');
|
|
||||||
console.log('[admin/debug-rsvp-config] 🔍 RSVP Table ID retrieved:', rsvpTableId);
|
|
||||||
|
|
||||||
// Test Events table ID retrieval
|
|
||||||
const eventsTableId = getEventTableId('Events');
|
|
||||||
console.log('[admin/debug-rsvp-config] 🔍 Events Table ID retrieved:', eventsTableId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
debug: {
|
|
||||||
effectiveConfig,
|
|
||||||
rsvpTableId,
|
|
||||||
eventsTableId,
|
|
||||||
availableTableKeys: Object.keys(effectiveConfig?.tables || {}),
|
|
||||||
rsvpTableInConfig: {
|
|
||||||
'rsvps': effectiveConfig?.tables?.['rsvps'],
|
|
||||||
'event_rsvps': effectiveConfig?.tables?.['event_rsvps'],
|
|
||||||
'EventRSVPs': effectiveConfig?.tables?.['EventRSVPs'],
|
|
||||||
'rsvp_table': effectiveConfig?.tables?.['rsvp_table'],
|
|
||||||
'RSVPs': effectiveConfig?.tables?.['RSVPs']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[admin/debug-rsvp-config] ❌ Error:', error);
|
|
||||||
|
|
||||||
if (error.statusCode) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw createError({
|
|
||||||
statusCode: 500,
|
|
||||||
statusMessage: 'Failed to debug RSVP configuration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
// server/api/admin/fix-event-attendee-counts.post.ts
|
|
||||||
import { createSessionManager } from '~/server/utils/session';
|
|
||||||
import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
try {
|
|
||||||
console.log('[admin/fix-event-attendee-counts] Starting attendee count fix...');
|
|
||||||
|
|
||||||
// Verify admin session
|
|
||||||
const sessionManager = createSessionManager();
|
|
||||||
const cookieHeader = getHeader(event, 'cookie');
|
|
||||||
const session = sessionManager.getSession(cookieHeader);
|
|
||||||
|
|
||||||
if (!session?.user || session.user.tier !== 'admin') {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 403,
|
|
||||||
statusMessage: 'Admin access required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[admin/fix-event-attendee-counts] Authorized admin user:', session.user.email);
|
|
||||||
|
|
||||||
const eventsClient = createNocoDBEventsClient();
|
|
||||||
|
|
||||||
// Get all events
|
|
||||||
const eventsResponse = await eventsClient.findAll({ limit: 1000 });
|
|
||||||
const events = eventsResponse.list || [];
|
|
||||||
|
|
||||||
console.log('[admin/fix-event-attendee-counts] Found', events.length, 'events to process');
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
// Process each event
|
|
||||||
for (const eventObj of events) {
|
|
||||||
try {
|
|
||||||
const eventId = eventObj.event_id || eventObj.id || (eventObj as any).Id;
|
|
||||||
const eventTitle = eventObj.title || 'Unknown Event';
|
|
||||||
|
|
||||||
console.log('[admin/fix-event-attendee-counts] Processing event:', eventTitle, 'ID:', eventId);
|
|
||||||
|
|
||||||
// Force update the attendee count
|
|
||||||
const newCount = await eventsClient.forceUpdateEventAttendeeCount(eventId.toString());
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
event_id: eventId,
|
|
||||||
title: eventTitle,
|
|
||||||
old_count: eventObj.current_attendees || 0,
|
|
||||||
new_count: newCount,
|
|
||||||
status: 'success'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[admin/fix-event-attendee-counts] ✅ Fixed event:', eventTitle, 'Count:', newCount);
|
|
||||||
|
|
||||||
} catch (eventError: any) {
|
|
||||||
console.error('[admin/fix-event-attendee-counts] ❌ Error processing event:', eventObj.title, eventError);
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
event_id: eventObj.event_id || eventObj.id,
|
|
||||||
title: eventObj.title || 'Unknown Event',
|
|
||||||
old_count: eventObj.current_attendees || 0,
|
|
||||||
new_count: 0,
|
|
||||||
status: 'error',
|
|
||||||
error: eventError.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const successCount = results.filter(r => r.status === 'success').length;
|
|
||||||
const errorCount = results.filter(r => r.status === 'error').length;
|
|
||||||
|
|
||||||
console.log('[admin/fix-event-attendee-counts] ✅ Fix completed. Success:', successCount, 'Errors:', errorCount);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `Fixed attendee counts for ${successCount} events (${errorCount} errors)`,
|
|
||||||
data: {
|
|
||||||
total_events: events.length,
|
|
||||||
success_count: successCount,
|
|
||||||
error_count: errorCount,
|
|
||||||
results: results
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[admin/fix-event-attendee-counts] ❌ Error:', error);
|
|
||||||
|
|
||||||
if (error.statusCode) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw createError({
|
|
||||||
statusCode: 500,
|
|
||||||
statusMessage: 'Failed to fix event attendee counts'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
import { getNocoDbConfiguration } from '~/server/utils/nocodb';
|
|
||||||
import { createSessionManager } from '~/server/utils/session';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
console.log('[api/admin/membership-status-fix] POST /api/admin/membership-status-fix');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate session and require admin privileges
|
|
||||||
const sessionManager = createSessionManager();
|
|
||||||
const cookieHeader = getCookie(event, 'monacousa-session') ? getHeader(event, 'cookie') : undefined;
|
|
||||||
const session = sessionManager.getSession(cookieHeader);
|
|
||||||
|
|
||||||
if (!session?.user) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 401,
|
|
||||||
statusMessage: 'Authentication required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.user.tier !== 'admin') {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 403,
|
|
||||||
statusMessage: 'Admin privileges required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[api/admin/membership-status-fix] Authorized admin:', session.user.email);
|
|
||||||
|
|
||||||
const body = await readBody(event);
|
|
||||||
const { action, tableId } = body;
|
|
||||||
|
|
||||||
const config = getNocoDbConfiguration();
|
|
||||||
|
|
||||||
if (action === 'check') {
|
|
||||||
return await checkMembershipStatusField(config, tableId);
|
|
||||||
} else if (action === 'fix') {
|
|
||||||
return await fixMembershipStatusField(config, tableId);
|
|
||||||
} else {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
statusMessage: 'Invalid action. Use "check" or "fix"'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[api/admin/membership-status-fix] Error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function checkMembershipStatusField(config: any, tableId: string) {
|
|
||||||
console.log('[checkMembershipStatusField] Checking membership status field configuration');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get table schema to check the membership_status field configuration
|
|
||||||
const tableSchema = await $fetch<any>(`${config.url}/api/v2/tables/${tableId}`, {
|
|
||||||
headers: {
|
|
||||||
"xc-token": config.token,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[checkMembershipStatusField] Table schema fetched');
|
|
||||||
|
|
||||||
// Find the membership_status field
|
|
||||||
const membershipStatusField = tableSchema.columns?.find((col: any) =>
|
|
||||||
col.column_name === 'membership_status' || col.title === 'Membership Status'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!membershipStatusField) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Membership Status field not found in table schema',
|
|
||||||
tableSchema: tableSchema.columns?.map((col: any) => ({
|
|
||||||
name: col.column_name,
|
|
||||||
title: col.title,
|
|
||||||
uidt: col.uidt
|
|
||||||
})) || []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[checkMembershipStatusField] Membership Status field found:', {
|
|
||||||
column_name: membershipStatusField.column_name,
|
|
||||||
title: membershipStatusField.title,
|
|
||||||
uidt: membershipStatusField.uidt,
|
|
||||||
dtxp: membershipStatusField.dtxp
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if it's a Single Select field and what options are available
|
|
||||||
if (membershipStatusField.uidt === 'SingleSelect') {
|
|
||||||
const options = membershipStatusField.colOptions?.options || [];
|
|
||||||
const allowedValues = options.map((opt: any) => opt.title);
|
|
||||||
|
|
||||||
const requiredValues = ['Active', 'Inactive', 'Pending', 'Expired'];
|
|
||||||
const missingValues = requiredValues.filter(val => !allowedValues.includes(val));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
fieldType: 'SingleSelect',
|
|
||||||
currentOptions: allowedValues,
|
|
||||||
requiredOptions: requiredValues,
|
|
||||||
missingOptions: missingValues,
|
|
||||||
needsFix: missingValues.length > 0,
|
|
||||||
fieldId: membershipStatusField.id,
|
|
||||||
message: missingValues.length > 0
|
|
||||||
? `Missing options: ${missingValues.join(', ')}`
|
|
||||||
: 'All required options are present'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
fieldType: membershipStatusField.uidt,
|
|
||||||
needsFix: false,
|
|
||||||
message: `Field type is ${membershipStatusField.uidt}, not SingleSelect. This should work fine.`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[checkMembershipStatusField] Error:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message || 'Failed to check field configuration'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fixMembershipStatusField(config: any, tableId: string) {
|
|
||||||
console.log('[fixMembershipStatusField] Fixing membership status field configuration');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First check the current state
|
|
||||||
const checkResult = await checkMembershipStatusField(config, tableId);
|
|
||||||
|
|
||||||
if (!checkResult.success) {
|
|
||||||
throw new Error(checkResult.error || 'Failed to check field configuration');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkResult.needsFix) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: 'No fix needed - all options are already present',
|
|
||||||
currentOptions: checkResult.currentOptions
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkResult.fieldType !== 'SingleSelect') {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Cannot fix field type ${checkResult.fieldType}. Only SingleSelect fields can be updated.`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the field to include all required options
|
|
||||||
const fieldId = checkResult.fieldId;
|
|
||||||
const currentOptions = checkResult.currentOptions || [];
|
|
||||||
const requiredOptions = ['Active', 'Inactive', 'Pending', 'Expired'];
|
|
||||||
|
|
||||||
// Create options array with all required values
|
|
||||||
const optionsToSet = requiredOptions.map((option, index) => ({
|
|
||||||
title: option,
|
|
||||||
color: getStatusColor(option),
|
|
||||||
order: index + 1
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log('[fixMembershipStatusField] Updating field with options:', optionsToSet);
|
|
||||||
|
|
||||||
// Update the field via NocoDB API
|
|
||||||
const updateResult = await $fetch(`${config.url}/api/v2/tables/${tableId}/columns/${fieldId}`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
"xc-token": config.token,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
colOptions: {
|
|
||||||
options: optionsToSet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[fixMembershipStatusField] Field updated successfully');
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: 'Membership Status field updated successfully',
|
|
||||||
addedOptions: checkResult.missingOptions,
|
|
||||||
allOptions: requiredOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[fixMembershipStatusField] Error:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message || 'Failed to fix field configuration'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusColor(status: string): string {
|
|
||||||
const colors = {
|
|
||||||
'Active': '#22c55e', // Green
|
|
||||||
'Inactive': '#6b7280', // Gray
|
|
||||||
'Pending': '#f59e0b', // Yellow
|
|
||||||
'Expired': '#ef4444' // Red
|
|
||||||
};
|
|
||||||
return colors[status as keyof typeof colors] || '#6b7280';
|
|
||||||
}
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
import type { NocoDBSettings } from '~/utils/types';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
console.log('[api/admin/nocodb-test.post] =========================');
|
|
||||||
console.log('[api/admin/nocodb-test.post] POST /api/admin/nocodb-test');
|
|
||||||
console.log('[api/admin/nocodb-test.post] Request from:', getClientIP(event));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check admin authorization
|
|
||||||
const sessionManager = createSessionManager();
|
|
||||||
const cookieHeader = getHeader(event, 'cookie');
|
|
||||||
const session = sessionManager.getSession(cookieHeader);
|
|
||||||
|
|
||||||
if (!session?.user) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 401,
|
|
||||||
statusMessage: 'Authentication required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is admin
|
|
||||||
if (session.user.tier !== 'admin') {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 403,
|
|
||||||
statusMessage: 'Admin access required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[api/admin/nocodb-test.post] Admin access confirmed for:', session.user.email);
|
|
||||||
|
|
||||||
// Get request body - this contains the settings to test
|
|
||||||
const body = await readBody(event) as NocoDBSettings;
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!body.url || !body.apiKey || !body.baseId || !body.tables || Object.keys(body.tables).length === 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'All fields are required to test connection (url, apiKey, baseId, and at least one table)'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate URL format
|
|
||||||
if (!body.url.startsWith('http://') && !body.url.startsWith('https://')) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'URL must start with http:// or https://'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate API token format - check for non-ASCII characters that would cause ByteString errors
|
|
||||||
const apiKey = body.apiKey.trim();
|
|
||||||
if (!/^[\x00-\xFF]*$/.test(apiKey)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'API token contains invalid characters. Please ensure you copied the token correctly without any special formatting characters.'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional validation for common token issues
|
|
||||||
if (apiKey.includes('•') || apiKey.includes('…') || apiKey.includes('"') || apiKey.includes('"')) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'API token contains formatting characters (bullets, quotes, etc.). Please copy the raw token from NocoDB without any formatting.'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[api/admin/nocodb-test.post] Testing NocoDB connection...');
|
|
||||||
console.log('[api/admin/nocodb-test.post] URL:', body.url);
|
|
||||||
console.log('[api/admin/nocodb-test.post] Base ID:', body.baseId);
|
|
||||||
console.log('[api/admin/nocodb-test.post] Tables:', Object.keys(body.tables));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test connection by making a simple request to NocoDB using the first table
|
|
||||||
const firstTableName = Object.keys(body.tables)[0];
|
|
||||||
const firstTableId = body.tables[firstTableName];
|
|
||||||
const testUrl = `${body.url}/api/v2/tables/${firstTableId}/records?limit=1`;
|
|
||||||
|
|
||||||
console.log('[api/admin/nocodb-test.post] Testing URL:', testUrl);
|
|
||||||
|
|
||||||
const response = await $fetch(testUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'xc-token': body.apiKey,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
// Add timeout to prevent hanging
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[api/admin/nocodb-test.post] ✅ Connection successful');
|
|
||||||
console.log('[api/admin/nocodb-test.post] Response received, type:', typeof response);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: 'Connection successful! NocoDB is responding.'
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (connectionError: any) {
|
|
||||||
console.error('[api/admin/nocodb-test.post] ❌ Connection failed:', connectionError);
|
|
||||||
|
|
||||||
let errorMessage = 'Connection failed';
|
|
||||||
|
|
||||||
if (connectionError.statusCode === 401 || connectionError.status === 401) {
|
|
||||||
errorMessage = 'Authentication failed - please check your API token';
|
|
||||||
} else if (connectionError.statusCode === 404 || connectionError.status === 404) {
|
|
||||||
errorMessage = 'Table not found - please check your Base ID and Table ID';
|
|
||||||
} else if (connectionError.statusCode === 403 || connectionError.status === 403) {
|
|
||||||
errorMessage = 'Access denied - please check your API token permissions';
|
|
||||||
} else if (connectionError.code === 'NETWORK_ERROR' || connectionError.message?.includes('fetch')) {
|
|
||||||
errorMessage = 'Network error - please check the URL and ensure NocoDB is accessible';
|
|
||||||
} else if (connectionError.message?.includes('timeout')) {
|
|
||||||
errorMessage = 'Connection timeout - NocoDB server may be unavailable';
|
|
||||||
} else if (connectionError.message) {
|
|
||||||
errorMessage = connectionError.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: errorMessage
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[api/admin/nocodb-test.post] ❌ Error:', error);
|
|
||||||
|
|
||||||
if (error.statusCode) {
|
|
||||||
throw error; // Re-throw HTTP errors
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'Failed to test connection due to server error'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
// Mobile detection and debugging utilities
|
|
||||||
|
|
||||||
export interface DeviceInfo {
|
|
||||||
isMobile: boolean;
|
|
||||||
isIOS: boolean;
|
|
||||||
isAndroid: boolean;
|
|
||||||
isTablet: boolean;
|
|
||||||
browser: string;
|
|
||||||
version: string;
|
|
||||||
userAgent: string;
|
|
||||||
cookieEnabled: boolean;
|
|
||||||
screenWidth: number;
|
|
||||||
screenHeight: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDeviceInfo(): DeviceInfo {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return {
|
|
||||||
isMobile: false,
|
|
||||||
isIOS: false,
|
|
||||||
isAndroid: false,
|
|
||||||
isTablet: false,
|
|
||||||
browser: 'Unknown',
|
|
||||||
version: 'Unknown',
|
|
||||||
userAgent: 'Server',
|
|
||||||
cookieEnabled: false,
|
|
||||||
screenWidth: 0,
|
|
||||||
screenHeight: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ua = navigator.userAgent;
|
|
||||||
|
|
||||||
// Mobile detection
|
|
||||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
|
||||||
const isIOS = /iPad|iPhone|iPod/.test(ua);
|
|
||||||
const isAndroid = /Android/.test(ua);
|
|
||||||
const isTablet = /iPad|Android(?=.*\bMobile\b)(?!.*\bMobile\b)/i.test(ua);
|
|
||||||
|
|
||||||
// Browser detection
|
|
||||||
let browser = 'Unknown';
|
|
||||||
let version = 'Unknown';
|
|
||||||
|
|
||||||
if (ua.includes('Chrome') && !ua.includes('Edge')) {
|
|
||||||
browser = 'Chrome';
|
|
||||||
const match = ua.match(/Chrome\/(\d+)/);
|
|
||||||
version = match ? match[1] : 'Unknown';
|
|
||||||
} else if (ua.includes('Safari') && !ua.includes('Chrome')) {
|
|
||||||
browser = 'Safari';
|
|
||||||
const match = ua.match(/Version\/(\d+)/);
|
|
||||||
version = match ? match[1] : 'Unknown';
|
|
||||||
} else if (ua.includes('Firefox')) {
|
|
||||||
browser = 'Firefox';
|
|
||||||
const match = ua.match(/Firefox\/(\d+)/);
|
|
||||||
version = match ? match[1] : 'Unknown';
|
|
||||||
} else if (ua.includes('Edge')) {
|
|
||||||
browser = 'Edge';
|
|
||||||
const match = ua.match(/Edge\/(\d+)/);
|
|
||||||
version = match ? match[1] : 'Unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isMobile,
|
|
||||||
isIOS,
|
|
||||||
isAndroid,
|
|
||||||
isTablet,
|
|
||||||
browser,
|
|
||||||
version,
|
|
||||||
userAgent: ua,
|
|
||||||
cookieEnabled: navigator.cookieEnabled,
|
|
||||||
screenWidth: window.screen.width,
|
|
||||||
screenHeight: window.screen.height
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logDeviceInfo(label: string = 'Device Info'): void {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
const info = getDeviceInfo();
|
|
||||||
|
|
||||||
console.group(`📱 ${label}`);
|
|
||||||
console.log('Mobile:', info.isMobile);
|
|
||||||
console.log('iOS:', info.isIOS);
|
|
||||||
console.log('Android:', info.isAndroid);
|
|
||||||
console.log('Tablet:', info.isTablet);
|
|
||||||
console.log('Browser:', `${info.browser} ${info.version}`);
|
|
||||||
console.log('Cookies Enabled:', info.cookieEnabled);
|
|
||||||
console.log('Screen:', `${info.screenWidth}x${info.screenHeight}`);
|
|
||||||
console.log('User Agent:', info.userAgent);
|
|
||||||
|
|
||||||
// Additional debugging info
|
|
||||||
console.log('Viewport:', `${window.innerWidth}x${window.innerHeight}`);
|
|
||||||
console.log('Device Pixel Ratio:', window.devicePixelRatio);
|
|
||||||
console.log('Online:', navigator.onLine);
|
|
||||||
console.log('Language:', navigator.language);
|
|
||||||
console.log('Platform:', navigator.platform);
|
|
||||||
|
|
||||||
// Cookie testing
|
|
||||||
try {
|
|
||||||
document.cookie = 'test=1; path=/';
|
|
||||||
const canSetCookie = document.cookie.includes('test=1');
|
|
||||||
console.log('Can Set Cookies:', canSetCookie);
|
|
||||||
if (canSetCookie) {
|
|
||||||
document.cookie = 'test=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Cookie Test Error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMobileDevice(): boolean {
|
|
||||||
return getDeviceInfo().isMobile;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isIOSDevice(): boolean {
|
|
||||||
return getDeviceInfo().isIOS;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isAndroidDevice(): boolean {
|
|
||||||
return getDeviceInfo().isAndroid;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMobileBrowser(): string {
|
|
||||||
const info = getDeviceInfo();
|
|
||||||
return `${info.browser} ${info.version}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhanced mobile login debugging
|
|
||||||
export function debugMobileLogin(context: string): void {
|
|
||||||
if (!isMobileDevice()) return;
|
|
||||||
|
|
||||||
console.group(`🔐 Mobile Login Debug - ${context}`);
|
|
||||||
logDeviceInfo('Current Device');
|
|
||||||
|
|
||||||
// Check for known mobile issues
|
|
||||||
const info = getDeviceInfo();
|
|
||||||
const issues: string[] = [];
|
|
||||||
|
|
||||||
if (info.isIOS && info.browser === 'Safari' && parseInt(info.version) < 14) {
|
|
||||||
issues.push('Safari < 14: Cookie issues with SameSite');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.isAndroid && info.browser === 'Chrome' && parseInt(info.version) < 80) {
|
|
||||||
issues.push('Chrome < 80: SameSite cookie support limited');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!info.cookieEnabled) {
|
|
||||||
issues.push('Cookies disabled in browser');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.screenWidth < 375) {
|
|
||||||
issues.push('Small screen may affect layout');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (issues.length > 0) {
|
|
||||||
console.warn('⚠️ Potential Issues:', issues);
|
|
||||||
} else {
|
|
||||||
console.log('✅ No known compatibility issues detected');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Network debugging
|
|
||||||
export function debugNetworkConditions(): void {
|
|
||||||
if (typeof navigator === 'undefined') return;
|
|
||||||
|
|
||||||
console.group('🌐 Network Conditions');
|
|
||||||
console.log('Online:', navigator.onLine);
|
|
||||||
|
|
||||||
// @ts-ignore - connection API is experimental
|
|
||||||
if (navigator.connection) {
|
|
||||||
// @ts-ignore
|
|
||||||
const conn = navigator.connection;
|
|
||||||
console.log('Connection Type:', conn.effectiveType);
|
|
||||||
console.log('Downlink:', conn.downlink, 'Mbps');
|
|
||||||
console.log('RTT:', conn.rtt, 'ms');
|
|
||||||
console.log('Save Data:', conn.saveData);
|
|
||||||
} else {
|
|
||||||
console.log('Network API not available');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
// PWA debugging
|
|
||||||
export function debugPWACapabilities(): void {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
console.group('📱 PWA Capabilities');
|
|
||||||
console.log('Service Worker:', 'serviceWorker' in navigator);
|
|
||||||
console.log('Web App Manifest:', 'onbeforeinstallprompt' in window);
|
|
||||||
console.log('Standalone Mode:', window.matchMedia('(display-mode: standalone)').matches);
|
|
||||||
console.log('Full Screen API:', 'requestFullscreen' in document.documentElement);
|
|
||||||
console.log('Notification API:', 'Notification' in window);
|
|
||||||
console.log('Push API:', 'PushManager' in window);
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete mobile debugging suite
|
|
||||||
export function runMobileDiagnostics(): void {
|
|
||||||
if (!isMobileDevice()) {
|
|
||||||
console.log('📱 Not a mobile device, skipping mobile diagnostics');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.group('🔍 Mobile Diagnostics Suite');
|
|
||||||
logDeviceInfo();
|
|
||||||
debugNetworkConditions();
|
|
||||||
debugPWACapabilities();
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
|
|
@ -1,310 +0,0 @@
|
||||||
/**
|
|
||||||
* Reload Loop Detection and Prevention Utility
|
|
||||||
* Advanced mobile Safari reload loop prevention system
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface PageLoadInfo {
|
|
||||||
url: string;
|
|
||||||
timestamp: number;
|
|
||||||
userAgent: string;
|
|
||||||
loadCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReloadLoopState {
|
|
||||||
enabled: boolean;
|
|
||||||
pageLoads: PageLoadInfo[];
|
|
||||||
blockedPages: Set<string>;
|
|
||||||
emergencyMode: boolean;
|
|
||||||
debugMode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RELOAD_LOOP_THRESHOLD = 5; // Max page loads in time window
|
|
||||||
const TIME_WINDOW = 10000; // 10 seconds
|
|
||||||
const EMERGENCY_BLOCK_TIME = 30000; // 30 seconds
|
|
||||||
const STORAGE_KEY = 'reload_loop_prevention';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or initialize reload loop state
|
|
||||||
*/
|
|
||||||
function getReloadLoopState(): ReloadLoopState {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return {
|
|
||||||
enabled: false,
|
|
||||||
pageLoads: [],
|
|
||||||
blockedPages: new Set(),
|
|
||||||
emergencyMode: false,
|
|
||||||
debugMode: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use sessionStorage for persistence across reloads
|
|
||||||
try {
|
|
||||||
const stored = sessionStorage.getItem(STORAGE_KEY);
|
|
||||||
if (stored) {
|
|
||||||
const parsed = JSON.parse(stored);
|
|
||||||
return {
|
|
||||||
...parsed,
|
|
||||||
blockedPages: new Set(parsed.blockedPages || [])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[reload-prevention] Failed to parse stored state:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize new state
|
|
||||||
const initialState: ReloadLoopState = {
|
|
||||||
enabled: true,
|
|
||||||
pageLoads: [],
|
|
||||||
blockedPages: new Set(),
|
|
||||||
emergencyMode: false,
|
|
||||||
debugMode: process.env.NODE_ENV === 'development'
|
|
||||||
};
|
|
||||||
|
|
||||||
saveReloadLoopState(initialState);
|
|
||||||
return initialState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save reload loop state to sessionStorage
|
|
||||||
*/
|
|
||||||
function saveReloadLoopState(state: ReloadLoopState): void {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const toStore = {
|
|
||||||
...state,
|
|
||||||
blockedPages: Array.from(state.blockedPages)
|
|
||||||
};
|
|
||||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[reload-prevention] Failed to save state:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record a page load and check for reload loops
|
|
||||||
*/
|
|
||||||
export function recordPageLoad(url: string): boolean {
|
|
||||||
const state = getReloadLoopState();
|
|
||||||
|
|
||||||
if (!state.enabled) {
|
|
||||||
return true; // Allow load
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const userAgent = navigator.userAgent || '';
|
|
||||||
|
|
||||||
// Clean old page loads
|
|
||||||
state.pageLoads = state.pageLoads.filter(
|
|
||||||
load => now - load.timestamp < TIME_WINDOW
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find existing load for this URL
|
|
||||||
const existingLoad = state.pageLoads.find(load => load.url === url);
|
|
||||||
|
|
||||||
if (existingLoad) {
|
|
||||||
existingLoad.loadCount++;
|
|
||||||
existingLoad.timestamp = now;
|
|
||||||
} else {
|
|
||||||
state.pageLoads.push({
|
|
||||||
url,
|
|
||||||
timestamp: now,
|
|
||||||
userAgent,
|
|
||||||
loadCount: 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for reload loop
|
|
||||||
const urlLoads = state.pageLoads.filter(load => load.url === url);
|
|
||||||
const totalLoads = urlLoads.reduce((sum, load) => sum + load.loadCount, 0);
|
|
||||||
|
|
||||||
if (totalLoads >= RELOAD_LOOP_THRESHOLD) {
|
|
||||||
console.error(`[reload-prevention] Reload loop detected for ${url} (${totalLoads} loads)`);
|
|
||||||
|
|
||||||
// Block this page
|
|
||||||
state.blockedPages.add(url);
|
|
||||||
state.emergencyMode = true;
|
|
||||||
|
|
||||||
// Set emergency timeout
|
|
||||||
setTimeout(() => {
|
|
||||||
state.emergencyMode = false;
|
|
||||||
state.blockedPages.delete(url);
|
|
||||||
saveReloadLoopState(state);
|
|
||||||
console.log(`[reload-prevention] Emergency block lifted for ${url}`);
|
|
||||||
}, EMERGENCY_BLOCK_TIME);
|
|
||||||
|
|
||||||
saveReloadLoopState(state);
|
|
||||||
return false; // Block load
|
|
||||||
}
|
|
||||||
|
|
||||||
saveReloadLoopState(state);
|
|
||||||
return true; // Allow load
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a page is currently blocked
|
|
||||||
*/
|
|
||||||
export function isPageBlocked(url: string): boolean {
|
|
||||||
const state = getReloadLoopState();
|
|
||||||
return state.blockedPages.has(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize reload loop prevention for a page
|
|
||||||
*/
|
|
||||||
export function initReloadLoopPrevention(pageName: string): boolean {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentUrl = window.location.pathname;
|
|
||||||
const canLoad = recordPageLoad(currentUrl);
|
|
||||||
|
|
||||||
if (!canLoad) {
|
|
||||||
console.error(`[reload-prevention] Page load blocked: ${pageName} (${currentUrl})`);
|
|
||||||
|
|
||||||
// Show emergency message
|
|
||||||
showEmergencyMessage(pageName);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getReloadLoopState().debugMode) {
|
|
||||||
console.log(`[reload-prevention] Page load allowed: ${pageName} (${currentUrl})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show emergency message when page is blocked
|
|
||||||
*/
|
|
||||||
function showEmergencyMessage(pageName: string): void {
|
|
||||||
const message = `
|
|
||||||
<div style="
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(163, 21, 21, 0.95);
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 10000;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
">
|
|
||||||
<h1 style="font-size: 24px; margin-bottom: 20px;">Page Loading Temporarily Blocked</h1>
|
|
||||||
<p style="font-size: 16px; margin-bottom: 20px;">
|
|
||||||
Multiple rapid page loads detected for ${pageName}.<br>
|
|
||||||
This is a safety measure to prevent infinite loading loops.
|
|
||||||
</p>
|
|
||||||
<p style="font-size: 14px; margin-bottom: 30px;">
|
|
||||||
The block will be automatically lifted in 30 seconds.
|
|
||||||
</p>
|
|
||||||
<button onclick="location.href='/'" style="
|
|
||||||
background: white;
|
|
||||||
color: #a31515;
|
|
||||||
border: none;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-right: 15px;
|
|
||||||
">Return to Home</button>
|
|
||||||
<button onclick="window.history.back()" style="
|
|
||||||
background: transparent;
|
|
||||||
color: white;
|
|
||||||
border: 2px solid white;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
">Go Back</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.innerHTML = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear reload loop prevention state (for testing)
|
|
||||||
*/
|
|
||||||
export function clearReloadLoopPrevention(): void {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
sessionStorage.removeItem(STORAGE_KEY);
|
|
||||||
console.log('[reload-prevention] State cleared');
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[reload-prevention] Failed to clear state:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current reload loop prevention status
|
|
||||||
*/
|
|
||||||
export function getReloadLoopStatus(): {
|
|
||||||
enabled: boolean;
|
|
||||||
emergencyMode: boolean;
|
|
||||||
blockedPages: string[];
|
|
||||||
pageLoads: PageLoadInfo[];
|
|
||||||
} {
|
|
||||||
const state = getReloadLoopState();
|
|
||||||
return {
|
|
||||||
enabled: state.enabled,
|
|
||||||
emergencyMode: state.emergencyMode,
|
|
||||||
blockedPages: Array.from(state.blockedPages),
|
|
||||||
pageLoads: state.pageLoads
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mobile Safari specific optimizations
|
|
||||||
*/
|
|
||||||
export function applyMobileSafariReloadLoopFixes(): void {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
const userAgent = navigator.userAgent || '';
|
|
||||||
const isMobileSafari = /iPad|iPhone|iPod/.test(userAgent) && /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent);
|
|
||||||
|
|
||||||
if (!isMobileSafari) return;
|
|
||||||
|
|
||||||
console.log('[reload-prevention] Applying mobile Safari specific fixes');
|
|
||||||
|
|
||||||
// Prevent Safari's aggressive caching that can cause reload loops
|
|
||||||
window.addEventListener('pageshow', (event) => {
|
|
||||||
if (event.persisted) {
|
|
||||||
console.log('[reload-prevention] Page restored from bfcache');
|
|
||||||
// Force a small delay to prevent immediate re-execution
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('[reload-prevention] bfcache restore handled');
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle Safari's navigation timing issues
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
// Mark navigation in progress to prevent reload loop detection
|
|
||||||
const state = getReloadLoopState();
|
|
||||||
state.enabled = false;
|
|
||||||
saveReloadLoopState(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-enable after navigation completes
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const state = getReloadLoopState();
|
|
||||||
state.enabled = true;
|
|
||||||
saveReloadLoopState(state);
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-initialize on module load
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
applyMobileSafariReloadLoopFixes();
|
|
||||||
}
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
/**
|
|
||||||
* Static Device Detection Utility
|
|
||||||
* Provides non-reactive device detection for Safari iOS reload loop prevention
|
|
||||||
* Uses direct navigator.userAgent analysis without creating Vue reactive dependencies
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface DeviceInfo {
|
|
||||||
isMobile: boolean;
|
|
||||||
isIos: boolean;
|
|
||||||
isSafari: boolean;
|
|
||||||
isMobileSafari: boolean;
|
|
||||||
isAndroid: boolean;
|
|
||||||
userAgent: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cachedDeviceInfo: DeviceInfo | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get static device information without creating reactive dependencies
|
|
||||||
* Results are cached to prevent multiple userAgent parsing
|
|
||||||
*/
|
|
||||||
export function getStaticDeviceInfo(): DeviceInfo {
|
|
||||||
// Return cached result if available
|
|
||||||
if (cachedDeviceInfo) {
|
|
||||||
return cachedDeviceInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only run on client-side
|
|
||||||
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
||||||
cachedDeviceInfo = {
|
|
||||||
isMobile: false,
|
|
||||||
isIos: false,
|
|
||||||
isSafari: false,
|
|
||||||
isMobileSafari: false,
|
|
||||||
isAndroid: false,
|
|
||||||
userAgent: ''
|
|
||||||
};
|
|
||||||
return cachedDeviceInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userAgent = navigator.userAgent;
|
|
||||||
|
|
||||||
// Device detection logic
|
|
||||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
|
||||||
const isIos = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
|
|
||||||
const isAndroid = /Android/i.test(userAgent);
|
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
|
|
||||||
const isMobileSafari = isIos && isSafari;
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
cachedDeviceInfo = {
|
|
||||||
isMobile,
|
|
||||||
isIos,
|
|
||||||
isSafari,
|
|
||||||
isMobileSafari,
|
|
||||||
isAndroid,
|
|
||||||
userAgent
|
|
||||||
};
|
|
||||||
|
|
||||||
return cachedDeviceInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get CSS classes for device-specific styling
|
|
||||||
* Returns a space-separated string of CSS classes
|
|
||||||
*/
|
|
||||||
export function getDeviceCssClasses(baseClass: string = ''): string {
|
|
||||||
const device = getStaticDeviceInfo();
|
|
||||||
const classes = [baseClass].filter(Boolean);
|
|
||||||
|
|
||||||
if (device.isMobile) classes.push('is-mobile');
|
|
||||||
if (device.isIos) classes.push('is-ios');
|
|
||||||
if (device.isSafari) classes.push('is-safari');
|
|
||||||
if (device.isMobileSafari) classes.push('is-mobile-safari');
|
|
||||||
if (device.isAndroid) classes.push('is-android');
|
|
||||||
|
|
||||||
return classes.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if current device is mobile Safari specifically
|
|
||||||
* This is the primary problematic browser for reload loops
|
|
||||||
*/
|
|
||||||
export function isMobileSafari(): boolean {
|
|
||||||
return getStaticDeviceInfo().isMobileSafari;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply mobile Safari specific optimizations to DOM element
|
|
||||||
* Should be called once per component to prevent reactive updates
|
|
||||||
*/
|
|
||||||
export function applyMobileSafariOptimizations(element?: HTMLElement): void {
|
|
||||||
if (!isMobileSafari()) return;
|
|
||||||
|
|
||||||
const targetElement = element || document.documentElement;
|
|
||||||
|
|
||||||
// Apply performance optimization classes
|
|
||||||
targetElement.classList.add('is-mobile-safari', 'performance-optimized');
|
|
||||||
|
|
||||||
// Set viewport height CSS variable for mobile Safari
|
|
||||||
const vh = window.innerHeight * 0.01;
|
|
||||||
targetElement.style.setProperty('--vh', `${vh}px`);
|
|
||||||
|
|
||||||
// Disable problematic CSS features for performance
|
|
||||||
targetElement.style.setProperty('--backdrop-filter', 'none');
|
|
||||||
targetElement.style.setProperty('--will-change', 'auto');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get viewport meta content optimized for mobile Safari
|
|
||||||
*/
|
|
||||||
export function getMobileSafariViewportMeta(): string {
|
|
||||||
const device = getStaticDeviceInfo();
|
|
||||||
|
|
||||||
if (device.isMobileSafari) {
|
|
||||||
// Prevent zoom on input focus for iOS Safari
|
|
||||||
return 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'width=device-width, initial-scale=1.0';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear cached device info (useful for testing)
|
|
||||||
*/
|
|
||||||
export function clearDeviceInfoCache(): void {
|
|
||||||
cachedDeviceInfo = null;
|
|
||||||
}
|
|
||||||
|
|
@ -1,300 +0,0 @@
|
||||||
/**
|
|
||||||
* Client-side verification state management with circuit breaker pattern
|
|
||||||
* Prevents endless reload loops on mobile browsers
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface VerificationAttempt {
|
|
||||||
token: string;
|
|
||||||
attempts: number;
|
|
||||||
lastAttempt: number;
|
|
||||||
maxAttempts: number;
|
|
||||||
status: 'pending' | 'success' | 'failed' | 'blocked';
|
|
||||||
errors: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'email_verification_state';
|
|
||||||
const MAX_ATTEMPTS_DEFAULT = 3;
|
|
||||||
const ATTEMPT_WINDOW = 5 * 60 * 1000; // 5 minutes
|
|
||||||
const CIRCUIT_BREAKER_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get verification state for a token
|
|
||||||
*/
|
|
||||||
export function getVerificationState(token: string): VerificationAttempt | null {
|
|
||||||
if (typeof window === 'undefined' || !token) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stored = sessionStorage.getItem(`${STORAGE_KEY}_${token.substring(0, 10)}`);
|
|
||||||
if (!stored) return null;
|
|
||||||
|
|
||||||
const state = JSON.parse(stored) as VerificationAttempt;
|
|
||||||
|
|
||||||
// Check if circuit breaker timeout has passed
|
|
||||||
const now = Date.now();
|
|
||||||
if (state.status === 'blocked' && (now - state.lastAttempt) > CIRCUIT_BREAKER_TIMEOUT) {
|
|
||||||
console.log('[verification-state] Circuit breaker timeout passed, resetting state');
|
|
||||||
clearVerificationState(token);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[verification-state] Failed to parse stored state:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize or update verification state
|
|
||||||
*/
|
|
||||||
export function initVerificationState(token: string, maxAttempts: number = MAX_ATTEMPTS_DEFAULT): VerificationAttempt {
|
|
||||||
if (typeof window === 'undefined' || !token) {
|
|
||||||
throw new Error('Cannot initialize verification state: no window or token');
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = getVerificationState(token);
|
|
||||||
if (existing) {
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state: VerificationAttempt = {
|
|
||||||
token,
|
|
||||||
attempts: 0,
|
|
||||||
lastAttempt: 0,
|
|
||||||
maxAttempts,
|
|
||||||
status: 'pending',
|
|
||||||
errors: []
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
sessionStorage.setItem(`${STORAGE_KEY}_${token.substring(0, 10)}`, JSON.stringify(state));
|
|
||||||
console.log('[verification-state] Initialized verification state for token');
|
|
||||||
return state;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[verification-state] Failed to save state:', error);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record a verification attempt
|
|
||||||
*/
|
|
||||||
export function recordAttempt(token: string, success: boolean = false, error?: string): VerificationAttempt {
|
|
||||||
if (typeof window === 'undefined' || !token) {
|
|
||||||
throw new Error('Cannot record attempt: no window or token');
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = getVerificationState(token) || initVerificationState(token);
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Check if we're within the attempt window
|
|
||||||
if (state.lastAttempt > 0 && (now - state.lastAttempt) > ATTEMPT_WINDOW) {
|
|
||||||
console.log('[verification-state] Attempt window expired, resetting counter');
|
|
||||||
state.attempts = 0;
|
|
||||||
state.errors = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
state.attempts++;
|
|
||||||
state.lastAttempt = now;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
state.status = 'success';
|
|
||||||
console.log('[verification-state] Verification successful, clearing state');
|
|
||||||
// Don't clear immediately - let the navigation complete first
|
|
||||||
setTimeout(() => clearVerificationState(token), 1000);
|
|
||||||
} else {
|
|
||||||
if (error) {
|
|
||||||
state.errors.push(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.attempts >= state.maxAttempts) {
|
|
||||||
state.status = 'blocked';
|
|
||||||
console.log(`[verification-state] Maximum attempts (${state.maxAttempts}) reached, blocking further attempts`);
|
|
||||||
} else {
|
|
||||||
state.status = 'failed';
|
|
||||||
console.log(`[verification-state] Attempt ${state.attempts}/${state.maxAttempts} failed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
sessionStorage.setItem(`${STORAGE_KEY}_${token.substring(0, 10)}`, JSON.stringify(state));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[verification-state] Failed to update state:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if verification should be blocked
|
|
||||||
*/
|
|
||||||
export function shouldBlockVerification(token: string): boolean {
|
|
||||||
if (typeof window === 'undefined' || !token) return false;
|
|
||||||
|
|
||||||
const state = getVerificationState(token);
|
|
||||||
if (!state) return false;
|
|
||||||
|
|
||||||
if (state.status === 'blocked') {
|
|
||||||
const timeRemaining = CIRCUIT_BREAKER_TIMEOUT - (Date.now() - state.lastAttempt);
|
|
||||||
if (timeRemaining > 0) {
|
|
||||||
console.log(`[verification-state] Verification blocked for ${Math.ceil(timeRemaining / 1000 / 60)} more minutes`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.status === 'success' || (state.attempts >= state.maxAttempts && state.status !== 'pending');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear verification state for a token
|
|
||||||
*/
|
|
||||||
export function clearVerificationState(token: string): void {
|
|
||||||
if (typeof window === 'undefined' || !token) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
sessionStorage.removeItem(`${STORAGE_KEY}_${token.substring(0, 10)}`);
|
|
||||||
console.log('[verification-state] Cleared verification state');
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[verification-state] Failed to clear state:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user-friendly status message
|
|
||||||
*/
|
|
||||||
export function getStatusMessage(state: VerificationAttempt | null): string {
|
|
||||||
if (!state) return '';
|
|
||||||
|
|
||||||
switch (state.status) {
|
|
||||||
case 'pending':
|
|
||||||
return '';
|
|
||||||
case 'success':
|
|
||||||
return 'Email verified successfully!';
|
|
||||||
case 'failed':
|
|
||||||
if (state.attempts === 1) {
|
|
||||||
return 'Verification failed. Retrying...';
|
|
||||||
}
|
|
||||||
return `Verification failed (${state.attempts}/${state.maxAttempts} attempts). ${state.maxAttempts - state.attempts} attempts remaining.`;
|
|
||||||
case 'blocked':
|
|
||||||
const timeRemaining = Math.ceil((CIRCUIT_BREAKER_TIMEOUT - (Date.now() - state.lastAttempt)) / 1000 / 60);
|
|
||||||
return `Too many failed attempts. Please wait ${timeRemaining} minutes before trying again, or contact support.`;
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Progressive navigation with fallbacks for mobile browsers
|
|
||||||
*/
|
|
||||||
export async function navigateWithFallback(url: string, options: { replace?: boolean } = {}): Promise<boolean> {
|
|
||||||
if (typeof window === 'undefined') return false;
|
|
||||||
|
|
||||||
console.log(`[verification-state] Attempting navigation to: ${url}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Method 1: Use Nuxt navigateTo
|
|
||||||
if (typeof navigateTo === 'function') {
|
|
||||||
console.log('[verification-state] Using navigateTo');
|
|
||||||
await navigateTo(url, options);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[verification-state] navigateTo failed:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Method 2: Use Vue Router (if available)
|
|
||||||
const nuxtApp = (window as any)?.$nuxt;
|
|
||||||
if (nuxtApp?.$router) {
|
|
||||||
console.log('[verification-state] Using Vue Router');
|
|
||||||
if (options.replace) {
|
|
||||||
await nuxtApp.$router.replace(url);
|
|
||||||
} else {
|
|
||||||
await nuxtApp.$router.push(url);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[verification-state] Vue Router failed:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 3: Direct window.location (mobile fallback)
|
|
||||||
console.log('[verification-state] Using window.location fallback');
|
|
||||||
if (options.replace) {
|
|
||||||
window.location.replace(url);
|
|
||||||
} else {
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mobile-specific delay before navigation to ensure stability
|
|
||||||
*/
|
|
||||||
export function getMobileNavigationDelay(): number {
|
|
||||||
if (typeof window === 'undefined') return 0;
|
|
||||||
|
|
||||||
// Detect mobile browsers
|
|
||||||
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
||||||
const isSafari = /Safari/i.test(navigator.userAgent) && !/Chrome/i.test(navigator.userAgent);
|
|
||||||
|
|
||||||
if (isMobile && isSafari) {
|
|
||||||
return 500; // Extra delay for Safari on iOS
|
|
||||||
} else if (isMobile) {
|
|
||||||
return 300; // Standard mobile delay
|
|
||||||
}
|
|
||||||
|
|
||||||
return 100; // Minimal delay for desktop
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up all expired verification states
|
|
||||||
*/
|
|
||||||
export function cleanupExpiredStates(): void {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const now = Date.now();
|
|
||||||
const keysToRemove: string[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < sessionStorage.length; i++) {
|
|
||||||
const key = sessionStorage.key(i);
|
|
||||||
if (!key?.startsWith(STORAGE_KEY)) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stored = sessionStorage.getItem(key);
|
|
||||||
if (!stored) continue;
|
|
||||||
|
|
||||||
const state = JSON.parse(stored) as VerificationAttempt;
|
|
||||||
|
|
||||||
// Remove states older than circuit breaker timeout
|
|
||||||
if ((now - state.lastAttempt) > CIRCUIT_BREAKER_TIMEOUT) {
|
|
||||||
keysToRemove.push(key);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Remove invalid stored data
|
|
||||||
keysToRemove.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keysToRemove.forEach(key => {
|
|
||||||
sessionStorage.removeItem(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (keysToRemove.length > 0) {
|
|
||||||
console.log(`[verification-state] Cleaned up ${keysToRemove.length} expired verification states`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[verification-state] Failed to cleanup expired states:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-cleanup on page load
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// Clean up immediately
|
|
||||||
cleanupExpiredStates();
|
|
||||||
|
|
||||||
// Clean up periodically
|
|
||||||
setInterval(cleanupExpiredStates, 5 * 60 * 1000); // Every 5 minutes
|
|
||||||
}
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
/**
|
|
||||||
* CSS-Only Viewport Management System
|
|
||||||
* Handles mobile Safari viewport height changes through CSS custom properties only,
|
|
||||||
* without triggering any Vue component reactivity.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class ViewportManager {
|
|
||||||
private static instance: ViewportManager;
|
|
||||||
private initialized = false;
|
|
||||||
private resizeTimeout: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
static getInstance(): ViewportManager {
|
|
||||||
if (!ViewportManager.instance) {
|
|
||||||
ViewportManager.instance = new ViewportManager();
|
|
||||||
}
|
|
||||||
return ViewportManager.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.initialized || typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
console.log('[ViewportManager] Initializing CSS-only viewport management');
|
|
||||||
|
|
||||||
// Static device detection (no reactive dependencies)
|
|
||||||
const userAgent = navigator.userAgent;
|
|
||||||
const isIOS = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
|
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
|
|
||||||
const isMobileSafari = isIOS && isSafari;
|
|
||||||
|
|
||||||
// Only apply to mobile Safari where viewport issues occur
|
|
||||||
if (!isMobileSafari) {
|
|
||||||
console.log('[ViewportManager] Not mobile Safari, skipping viewport management');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastHeight = window.innerHeight;
|
|
||||||
let initialHeight = window.innerHeight;
|
|
||||||
let keyboardOpen = false;
|
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
// Skip if document is hidden (tab not active)
|
|
||||||
if (document.hidden) return;
|
|
||||||
|
|
||||||
// Clear any existing timeout
|
|
||||||
if (this.resizeTimeout) {
|
|
||||||
clearTimeout(this.resizeTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce with longer delay for mobile Safari
|
|
||||||
this.resizeTimeout = setTimeout(() => {
|
|
||||||
const newHeight = window.innerHeight;
|
|
||||||
const heightDiff = newHeight - lastHeight;
|
|
||||||
const absoluteDiff = Math.abs(heightDiff);
|
|
||||||
|
|
||||||
// Detect keyboard open/close patterns
|
|
||||||
if (heightDiff < -100 && newHeight < initialHeight * 0.75) {
|
|
||||||
keyboardOpen = true;
|
|
||||||
console.log('[ViewportManager] Keyboard opened, skipping update');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (heightDiff > 100 && keyboardOpen) {
|
|
||||||
keyboardOpen = false;
|
|
||||||
console.log('[ViewportManager] Keyboard closed, skipping update');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update for significant non-keyboard changes
|
|
||||||
const isOrientationChange = absoluteDiff > initialHeight * 0.3;
|
|
||||||
const isSignificantChange = absoluteDiff > 50;
|
|
||||||
|
|
||||||
if (isOrientationChange || (isSignificantChange && !keyboardOpen)) {
|
|
||||||
lastHeight = newHeight;
|
|
||||||
|
|
||||||
// Update CSS custom property only - no Vue reactivity
|
|
||||||
const vh = newHeight * 0.01;
|
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
|
||||||
|
|
||||||
console.log('[ViewportManager] Updated --vh to:', `${vh}px`);
|
|
||||||
|
|
||||||
// Update initial height after orientation change
|
|
||||||
if (isOrientationChange) {
|
|
||||||
initialHeight = newHeight;
|
|
||||||
console.log('[ViewportManager] Orientation change detected, updated initial height');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 300); // Longer debounce for mobile Safari
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set initial CSS custom property
|
|
||||||
const initialVh = initialHeight * 0.01;
|
|
||||||
document.documentElement.style.setProperty('--vh', `${initialVh}px`);
|
|
||||||
console.log('[ViewportManager] Set initial --vh to:', `${initialVh}px`);
|
|
||||||
|
|
||||||
// Add resize listener with passive option for better performance
|
|
||||||
window.addEventListener('resize', handleResize, { passive: true });
|
|
||||||
|
|
||||||
// Also listen for orientation changes on mobile
|
|
||||||
window.addEventListener('orientationchange', () => {
|
|
||||||
keyboardOpen = false; // Reset keyboard state on orientation change
|
|
||||||
console.log('[ViewportManager] Orientation change event, scheduling resize handler');
|
|
||||||
// Wait for orientation change to complete
|
|
||||||
setTimeout(handleResize, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add visibility change listener to pause updates when tab is hidden
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.hidden && this.resizeTimeout) {
|
|
||||||
clearTimeout(this.resizeTimeout);
|
|
||||||
this.resizeTimeout = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
console.log('[ViewportManager] Initialization complete');
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if (this.resizeTimeout) {
|
|
||||||
clearTimeout(this.resizeTimeout);
|
|
||||||
this.resizeTimeout = null;
|
|
||||||
}
|
|
||||||
this.initialized = false;
|
|
||||||
console.log('[ViewportManager] Cleanup complete');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export singleton instance
|
|
||||||
export const viewportManager = ViewportManager.getInstance();
|
|
||||||
|
|
||||||
// Auto-initialize on client side
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// Initialize after DOM is ready
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
viewportManager.init();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// DOM is already ready
|
|
||||||
viewportManager.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue