Add PWA support with install banner and app icons
Build And Push Image / docker (push) Successful in 2m56s
Details
Build And Push Image / docker (push) Successful in 2m56s
Details
- Configure @vite-pwa/nuxt module with manifest and service worker - Add PWA install banner component to login page - Include app icons (192x192, 512x512) and favicon assets - Update admin dashboard layout and remove backup section - Add PWA-related API endpoints and utility scripts
This commit is contained in:
parent
91cbffe189
commit
d0c9c02bf9
|
|
@ -0,0 +1,239 @@
|
||||||
|
<template>
|
||||||
|
<v-card
|
||||||
|
v-if="showBanner"
|
||||||
|
class="pwa-install-banner"
|
||||||
|
elevation="8"
|
||||||
|
color="primary"
|
||||||
|
variant="elevated"
|
||||||
|
>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-row align="center" no-gutters>
|
||||||
|
<v-col cols="auto" class="mr-3">
|
||||||
|
<v-avatar size="48" color="white">
|
||||||
|
<v-img src="/icon-192x192.png" alt="MonacoUSA Portal" />
|
||||||
|
</v-avatar>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col>
|
||||||
|
<div class="text-white">
|
||||||
|
<div class="text-subtitle-1 font-weight-bold mb-1">
|
||||||
|
Install MonacoUSA Portal
|
||||||
|
</div>
|
||||||
|
<div class="text-body-2 text-grey-lighten-2">
|
||||||
|
{{ installMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn
|
||||||
|
v-if="canInstall"
|
||||||
|
@click="installPWA"
|
||||||
|
color="white"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="mr-2"
|
||||||
|
:loading="installing"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-download</v-icon>
|
||||||
|
Install
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
@click="dismissBanner"
|
||||||
|
color="white"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
icon
|
||||||
|
>
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface BeforeInstallPromptEvent extends Event {
|
||||||
|
prompt(): Promise<void>;
|
||||||
|
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reactive state
|
||||||
|
const showBanner = ref(false);
|
||||||
|
const canInstall = ref(false);
|
||||||
|
const installing = ref(false);
|
||||||
|
const installMessage = ref('Add to your home screen for quick access');
|
||||||
|
let deferredPrompt: BeforeInstallPromptEvent | null = null;
|
||||||
|
|
||||||
|
// Device detection
|
||||||
|
const isIOS = computed(() => {
|
||||||
|
if (process.client) {
|
||||||
|
return /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAndroid = computed(() => {
|
||||||
|
if (process.client) {
|
||||||
|
return /Android/.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isStandalone = computed(() => {
|
||||||
|
if (process.client) {
|
||||||
|
return window.matchMedia('(display-mode: standalone)').matches ||
|
||||||
|
(window.navigator as any).standalone === true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install messages based on platform
|
||||||
|
const getInstallMessage = () => {
|
||||||
|
if (isIOS.value) {
|
||||||
|
return 'Tap Share → Add to Home Screen to install';
|
||||||
|
} else if (isAndroid.value) {
|
||||||
|
return 'Add to your home screen for quick access';
|
||||||
|
} else {
|
||||||
|
return 'Install this app for a better experience';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// PWA installation logic
|
||||||
|
const installPWA = async () => {
|
||||||
|
if (!deferredPrompt) return;
|
||||||
|
|
||||||
|
installing.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Show the install prompt
|
||||||
|
await deferredPrompt.prompt();
|
||||||
|
|
||||||
|
// Wait for the user to respond to the prompt
|
||||||
|
const { outcome } = await deferredPrompt.userChoice;
|
||||||
|
|
||||||
|
console.log(`PWA install prompt outcome: ${outcome}`);
|
||||||
|
|
||||||
|
if (outcome === 'accepted') {
|
||||||
|
console.log('✅ PWA installation accepted');
|
||||||
|
showBanner.value = false;
|
||||||
|
localStorage.setItem('pwa-install-dismissed', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the deferredPrompt
|
||||||
|
deferredPrompt = null;
|
||||||
|
canInstall.value = false;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ PWA installation error:', error);
|
||||||
|
} finally {
|
||||||
|
installing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismissBanner = () => {
|
||||||
|
showBanner.value = false;
|
||||||
|
localStorage.setItem('pwa-install-dismissed', 'true');
|
||||||
|
localStorage.setItem('pwa-install-dismissed-date', new Date().toISOString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldShowBanner = () => {
|
||||||
|
// Don't show if already dismissed recently (within 7 days)
|
||||||
|
const dismissedDate = localStorage.getItem('pwa-install-dismissed-date');
|
||||||
|
if (dismissedDate) {
|
||||||
|
const daysSinceDismissed = (Date.now() - new Date(dismissedDate).getTime()) / (1000 * 60 * 60 * 24);
|
||||||
|
if (daysSinceDismissed < 7) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show if permanently dismissed
|
||||||
|
if (localStorage.getItem('pwa-install-dismissed') === 'true' && !dismissedDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show if already installed
|
||||||
|
if (isStandalone.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup event listeners
|
||||||
|
onMounted(() => {
|
||||||
|
if (!process.client) return;
|
||||||
|
|
||||||
|
installMessage.value = getInstallMessage();
|
||||||
|
|
||||||
|
// Listen for the beforeinstallprompt event
|
||||||
|
window.addEventListener('beforeinstallprompt', (e: Event) => {
|
||||||
|
console.log('🔔 PWA install prompt available');
|
||||||
|
|
||||||
|
// Prevent the mini-infobar from appearing on mobile
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Save the event so it can be triggered later
|
||||||
|
deferredPrompt = e as BeforeInstallPromptEvent;
|
||||||
|
canInstall.value = true;
|
||||||
|
|
||||||
|
// Show banner if conditions are met
|
||||||
|
if (shouldShowBanner()) {
|
||||||
|
showBanner.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for successful installation
|
||||||
|
window.addEventListener('appinstalled', () => {
|
||||||
|
console.log('✅ PWA was installed successfully');
|
||||||
|
showBanner.value = false;
|
||||||
|
deferredPrompt = null;
|
||||||
|
canInstall.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// For iOS devices, show banner if not installed and not dismissed
|
||||||
|
if (isIOS.value && shouldShowBanner()) {
|
||||||
|
showBanner.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pwa-install-banner {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.pwa-install-banner {
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
.pwa-install-banner {
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -14,7 +14,60 @@ export default defineNuxtConfig({
|
||||||
console.log(`🌐 Server listening on http://${host}:${port}`)
|
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: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
titleTemplate: "%s • MonacoUSA Portal",
|
titleTemplate: "%s • MonacoUSA Portal",
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@
|
||||||
|
|
||||||
<!-- System Metrics -->
|
<!-- System Metrics -->
|
||||||
<v-row class="mb-6">
|
<v-row class="mb-6">
|
||||||
<v-col cols="12" md="8">
|
<v-col cols="12">
|
||||||
<v-card elevation="2">
|
<v-card elevation="2">
|
||||||
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
<v-icon class="mr-2" color="primary">mdi-chart-line</v-icon>
|
<v-icon class="mr-2" color="primary">mdi-chart-line</v-icon>
|
||||||
|
|
@ -135,35 +135,6 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="4">
|
|
||||||
<v-card elevation="2">
|
|
||||||
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
|
||||||
<v-icon class="mr-2" color="primary">mdi-backup-restore</v-icon>
|
|
||||||
Last Backup
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text class="pa-4">
|
|
||||||
<div class="text-h6 mb-2">System Backup</div>
|
|
||||||
<div class="text-body-2 mb-2">
|
|
||||||
<v-icon size="small" class="mr-1">mdi-calendar</v-icon>
|
|
||||||
{{ lastBackup.date }}
|
|
||||||
</div>
|
|
||||||
<div class="text-body-2 mb-4">
|
|
||||||
<v-icon size="small" class="mr-1">mdi-clock</v-icon>
|
|
||||||
{{ lastBackup.time }}
|
|
||||||
</div>
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
style="border-color: #a31515; color: #a31515;"
|
|
||||||
@click="initiateBackup"
|
|
||||||
>
|
|
||||||
Create Backup
|
|
||||||
</v-btn>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<!-- Recent Admin Activity -->
|
<!-- Recent Admin Activity -->
|
||||||
|
|
@ -359,10 +330,6 @@ const systemStats = ref({
|
||||||
uptime: '0d'
|
uptime: '0d'
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastBackup = ref({
|
|
||||||
date: 'January 7, 2025',
|
|
||||||
time: '3:00 AM EST'
|
|
||||||
});
|
|
||||||
|
|
||||||
const systemHealth = ref({
|
const systemHealth = ref({
|
||||||
cpu: 45,
|
cpu: 45,
|
||||||
|
|
@ -460,9 +427,6 @@ const navigateToSystemConfig = () => {
|
||||||
console.log('Navigate to system config');
|
console.log('Navigate to system config');
|
||||||
};
|
};
|
||||||
|
|
||||||
const initiateBackup = () => {
|
|
||||||
console.log('Initiate system backup');
|
|
||||||
};
|
|
||||||
|
|
||||||
const createNewUser = () => {
|
const createNewUser = () => {
|
||||||
console.log('Create new user');
|
console.log('Create new user');
|
||||||
|
|
@ -487,8 +451,8 @@ const systemMaintenance = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-card:hover {
|
.v-card:hover {
|
||||||
transform: translateY(-2px);
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-btn {
|
.v-btn {
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,9 @@
|
||||||
v-model="showForgotPassword"
|
v-model="showForgotPassword"
|
||||||
@success="handlePasswordResetSuccess"
|
@success="handlePasswordResetSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- PWA Install Banner -->
|
||||||
|
<PWAInstallBanner />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
|
|
@ -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();
|
||||||
|
|
@ -17,11 +17,11 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
console.log('✅ Admin access verified for user:', session.user.email);
|
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 = {
|
const stats = {
|
||||||
totalUsers: 156,
|
totalUsers: 156, // TODO: Get from Keycloak API
|
||||||
activeUsers: 45,
|
activeUsers: 45, // TODO: Get from session store
|
||||||
totalSessions: 67,
|
totalSessions: 67, // TODO: Get from session store
|
||||||
systemHealth: 'healthy',
|
systemHealth: 'healthy',
|
||||||
lastBackup: new Date().toISOString(),
|
lastBackup: new Date().toISOString(),
|
||||||
diskUsage: '45%',
|
diskUsage: '45%',
|
||||||
|
|
|
||||||
|
|
@ -94,20 +94,53 @@ export default defineEventHandler(async (event) => {
|
||||||
console.log('👤 Found user:', { id: userId, email: users[0].email });
|
console.log('👤 Found user:', { id: userId, email: users[0].email });
|
||||||
|
|
||||||
// Send reset password email using Keycloak's execute-actions-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',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${adminToken.access_token}`,
|
'Authorization': `Bearer ${adminToken.access_token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'User-Agent': 'MonacoUSA-Portal/1.0'
|
'User-Agent': 'MonacoUSA-Portal/1.0'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(['UPDATE_PASSWORD'])
|
body: JSON.stringify(['UPDATE_PASSWORD']),
|
||||||
|
signal: controller.signal
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!resetResponse.ok) {
|
if (!resetResponse.ok) {
|
||||||
console.error('❌ Failed to send reset email:', resetResponse.status);
|
console.error('❌ Failed to send reset email:', resetResponse.status);
|
||||||
const errorText = await resetResponse.text().catch(() => 'Unknown error');
|
const errorText = await resetResponse.text().catch(() => 'Unknown error');
|
||||||
console.error('Reset email error details:', errorText);
|
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');
|
throw new Error('Failed to send reset email');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,6 +154,24 @@ export default defineEventHandler(async (event) => {
|
||||||
} catch (keycloakError: any) {
|
} catch (keycloakError: any) {
|
||||||
console.error('❌ Keycloak API error:', keycloakError);
|
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
|
// For security, don't reveal specific errors to the user
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue