diff --git a/components/PWAInstallBanner.vue b/components/PWAInstallBanner.vue new file mode 100644 index 0000000..27457b7 --- /dev/null +++ b/components/PWAInstallBanner.vue @@ -0,0 +1,239 @@ + + + + + diff --git a/nuxt.config.ts b/nuxt.config.ts index 54b780b..9fc9406 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -14,7 +14,60 @@ export default defineNuxtConfig({ console.log(`🌐 Server listening on http://${host}:${port}`) } }, - modules: ["vuetify-nuxt-module", "motion-v/nuxt"], + modules: [ + "vuetify-nuxt-module", + "motion-v/nuxt", + [ + "@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' + } + ] + } + } + ] + ], app: { head: { titleTemplate: "%s • MonacoUSA Portal", diff --git a/pages/dashboard/admin.vue b/pages/dashboard/admin.vue index 48da3c3..9907738 100644 --- a/pages/dashboard/admin.vue +++ b/pages/dashboard/admin.vue @@ -107,7 +107,7 @@ - + mdi-chart-line @@ -135,35 +135,6 @@ - - - - - mdi-backup-restore - Last Backup - - -
System Backup
-
- mdi-calendar - {{ lastBackup.date }} -
-
- mdi-clock - {{ lastBackup.time }} -
- - Create Backup - -
-
-
@@ -359,10 +330,6 @@ const systemStats = ref({ uptime: '0d' }); -const lastBackup = ref({ - date: 'January 7, 2025', - time: '3:00 AM EST' -}); const systemHealth = ref({ cpu: 45, @@ -460,9 +427,6 @@ const navigateToSystemConfig = () => { console.log('Navigate to system config'); }; -const initiateBackup = () => { - console.log('Initiate system backup'); -}; const createNewUser = () => { console.log('Create new user'); @@ -487,8 +451,8 @@ const systemMaintenance = () => { } .v-card:hover { - transform: translateY(-2px); - transition: transform 0.2s ease-in-out; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + transition: box-shadow 0.2s ease; } .v-btn { diff --git a/pages/login.vue b/pages/login.vue index 7c40663..f3252b8 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -114,6 +114,9 @@ v-model="showForgotPassword" @success="handlePasswordResetSuccess" /> + + + diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..951e456 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..b60117a Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/icon-192x192.png b/public/icon-192x192.png new file mode 100644 index 0000000..203f490 Binary files /dev/null and b/public/icon-192x192.png differ diff --git a/public/icon-512x512.png b/public/icon-512x512.png new file mode 100644 index 0000000..8c611a2 Binary files /dev/null and b/public/icon-512x512.png differ diff --git a/scripts/generate-pwa-icons.cjs b/scripts/generate-pwa-icons.cjs new file mode 100644 index 0000000..6277774 --- /dev/null +++ b/scripts/generate-pwa-icons.cjs @@ -0,0 +1,64 @@ +const sharp = require('sharp'); +const fs = require('fs'); +const path = require('path'); + +async function generatePWAIcons() { + const inputFile = path.join(__dirname, '../public/MONACOUSA-Flags_376x376.png'); + const publicDir = path.join(__dirname, '../public'); + + console.log('🎨 Generating PWA icons from MonacoUSA logo...'); + + try { + // Generate 192x192 icon + await sharp(inputFile) + .resize(192, 192, { + fit: 'contain', + background: { r: 255, g: 255, b: 255, alpha: 0 } + }) + .png() + .toFile(path.join(publicDir, 'icon-192x192.png')); + + console.log('✅ Generated icon-192x192.png'); + + // Generate 512x512 icon + await sharp(inputFile) + .resize(512, 512, { + fit: 'contain', + background: { r: 255, g: 255, b: 255, alpha: 0 } + }) + .png() + .toFile(path.join(publicDir, 'icon-512x512.png')); + + console.log('✅ Generated icon-512x512.png'); + + // Generate Apple touch icon (180x180) + await sharp(inputFile) + .resize(180, 180, { + fit: 'contain', + background: { r: 255, g: 255, b: 255, alpha: 1 } + }) + .png() + .toFile(path.join(publicDir, 'apple-touch-icon.png')); + + console.log('✅ Generated apple-touch-icon.png'); + + // Generate favicon (32x32) + await sharp(inputFile) + .resize(32, 32, { + fit: 'contain', + background: { r: 255, g: 255, b: 255, alpha: 0 } + }) + .png() + .toFile(path.join(publicDir, 'favicon-32x32.png')); + + console.log('✅ Generated favicon-32x32.png'); + + console.log('🎉 All PWA icons generated successfully!'); + + } catch (error) { + console.error('❌ Error generating PWA icons:', error); + process.exit(1); + } +} + +generatePWAIcons(); diff --git a/server/api/admin/stats.get.ts b/server/api/admin/stats.get.ts index 8241fe9..ee77e56 100644 --- a/server/api/admin/stats.get.ts +++ b/server/api/admin/stats.get.ts @@ -17,11 +17,11 @@ export default defineEventHandler(async (event) => { console.log('✅ Admin access verified for user:', session.user.email); - // For now, return mock data - later integrate with actual data sources + // For now, return improved mock data - TODO: integrate with real data sources const stats = { - totalUsers: 156, - activeUsers: 45, - totalSessions: 67, + totalUsers: 156, // TODO: Get from Keycloak API + activeUsers: 45, // TODO: Get from session store + totalSessions: 67, // TODO: Get from session store systemHealth: 'healthy', lastBackup: new Date().toISOString(), diskUsage: '45%', diff --git a/server/api/auth/forgot-password.post.ts b/server/api/auth/forgot-password.post.ts index f1f442f..5c7d981 100644 --- a/server/api/auth/forgot-password.post.ts +++ b/server/api/auth/forgot-password.post.ts @@ -94,20 +94,53 @@ export default defineEventHandler(async (event) => { console.log('👤 Found user:', { id: userId, email: users[0].email }); // Send reset password email using Keycloak's execute-actions-email - const resetResponse = await fetch(`${adminBaseUrl}/users/${userId}/execute-actions-email`, { + // Add query parameters for better email template rendering + const resetUrl = new URL(`${adminBaseUrl}/users/${userId}/execute-actions-email`); + resetUrl.searchParams.set('clientId', config.keycloak.clientId); + resetUrl.searchParams.set('redirectUri', `${config.keycloak.callbackUrl.replace('/auth/callback', '/login')}`); + resetUrl.searchParams.set('lifespan', '43200'); // 12 hours + + console.log('🔄 Sending password reset email with parameters:', { + clientId: config.keycloak.clientId, + redirectUri: resetUrl.searchParams.get('redirectUri'), + lifespan: resetUrl.searchParams.get('lifespan') + }); + + // Create AbortController for timeout handling + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout + + const resetResponse = await fetch(resetUrl.toString(), { method: 'PUT', headers: { 'Authorization': `Bearer ${adminToken.access_token}`, 'Content-Type': 'application/json', 'User-Agent': 'MonacoUSA-Portal/1.0' }, - body: JSON.stringify(['UPDATE_PASSWORD']) + body: JSON.stringify(['UPDATE_PASSWORD']), + signal: controller.signal }); + clearTimeout(timeoutId); + if (!resetResponse.ok) { console.error('❌ Failed to send reset email:', resetResponse.status); const errorText = await resetResponse.text().catch(() => 'Unknown error'); console.error('Reset email error details:', errorText); + + // Enhanced error handling for different scenarios + if (resetResponse.status === 500) { + console.error('🚨 SMTP server error detected - this usually indicates email configuration issues in Keycloak'); + console.error('💡 Suggestion: Check Keycloak Admin Console → Realm Settings → Email tab'); + + // For now, still return success to user for security, but log the issue + console.log('🔄 Returning success message to user despite email failure for security'); + return { + success: true, + message: 'If the email exists in our system, a reset link has been sent. If you don\'t receive an email, please contact your administrator.' + }; + } + throw new Error('Failed to send reset email'); } @@ -121,6 +154,24 @@ export default defineEventHandler(async (event) => { } catch (keycloakError: any) { console.error('❌ Keycloak API error:', keycloakError); + // Handle timeout errors specifically + if (keycloakError.name === 'AbortError') { + console.error('⏰ Password reset request timed out after 30 seconds'); + return { + success: true, + message: 'Password reset request is being processed. If the email exists in our system, a reset link will be sent shortly.' + }; + } + + // Handle SMTP/email server errors + if (keycloakError.message?.includes('send reset email') || keycloakError.message?.includes('SMTP')) { + console.error('📧 Email server error detected, but user search was successful'); + return { + success: true, + message: 'If the email exists in our system, a reset link has been sent. If you don\'t receive an email, please contact your administrator.' + }; + } + // For security, don't reveal specific errors to the user return { success: true,