diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index eb742de6..8302d22a 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -153,9 +153,13 @@ export const authOptions: NextAuthOptions = { }, }, }, + jwt: { + // Add explicit max size to prevent chunking + maxAge: 30 * 24 * 60 * 60, // 30 days + }, callbacks: { async jwt({ token, account, profile }) { - // Only include essential data in the JWT to reduce size + // Drastically reduce JWT size by only storing essential info if (account && profile) { const keycloakProfile = profile as KeycloakProfile; const roles = keycloakProfile.realm_access?.roles || []; @@ -163,14 +167,21 @@ export const authOptions: NextAuthOptions = { role.replace(/^ROLE_/, '').toLowerCase() ); + // Store minimal data in the token token.accessToken = account.access_token ?? ''; token.refreshToken = account.refresh_token ?? ''; token.accessTokenExpires = account.expires_at ?? 0; token.sub = keycloakProfile.sub; token.role = cleanRoles; token.username = keycloakProfile.preferred_username ?? ''; - token.first_name = keycloakProfile.given_name ?? ''; - token.last_name = keycloakProfile.family_name ?? ''; + + // Only store these if they're short + if (keycloakProfile.given_name && keycloakProfile.given_name.length < 30) { + token.first_name = keycloakProfile.given_name; + } + if (keycloakProfile.family_name && keycloakProfile.family_name.length < 30) { + token.last_name = keycloakProfile.family_name; + } } else if (token.accessToken) { try { const decoded = jwtDecode(token.accessToken); @@ -199,7 +210,7 @@ export const authOptions: NextAuthOptions = { const userRoles = Array.isArray(token.role) ? token.role : []; - // Only include essential user data + // Create a minimal user object session.user = { id: token.sub ?? '', email: token.email ?? null, @@ -212,7 +223,7 @@ export const authOptions: NextAuthOptions = { nextcloudInitialized: false, }; - // Only pass the access token, not the entire token + // Only store access token, not the entire token session.accessToken = token.accessToken; return session; diff --git a/app/api/auth/session-cleanup/route.ts b/app/api/auth/session-cleanup/route.ts new file mode 100644 index 00000000..abbf1f69 --- /dev/null +++ b/app/api/auth/session-cleanup/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { cleanupUserSessions } from '@/lib/redis'; +import { closeUserImapConnections } from '@/lib/services/email-service'; + +/** + * API endpoint to clean up user sessions and invalidate cached data + * Called during logout to ensure proper cleanup of all connections + */ +export async function POST(request: NextRequest) { + try { + // Get the user ID either from the session or request body + const session = await getServerSession(authOptions); + const body = await request.json().catch(() => ({})); + + // Get user ID from session or from request body + const userId = session?.user?.id || body.userId; + + if (!userId) { + return NextResponse.json({ + success: false, + error: 'No user ID provided or user not authenticated' + }, { status: 400 }); + } + + console.log(`Processing session cleanup for user ${userId}`); + + // 1. Close any active IMAP connections using the dedicated function + const closedConnections = await closeUserImapConnections(userId); + + // 2. Clean up Redis data + await cleanupUserSessions(userId); + + // 3. Return success response with details + return NextResponse.json({ + success: true, + message: `Session cleanup completed for user ${userId}`, + details: { + closedConnections, + redisCleanupPerformed: true + } + }); + } catch (error) { + console.error('Error in session cleanup:', error); + return NextResponse.json({ + success: false, + error: 'Session cleanup failed', + details: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/components/responsive-iframe.tsx b/app/components/responsive-iframe.tsx index d1b128d1..da1ee02f 100644 --- a/app/components/responsive-iframe.tsx +++ b/app/components/responsive-iframe.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { useSession } from 'next-auth/react'; interface ResponsiveIframeProps { src: string; @@ -12,11 +13,49 @@ interface ResponsiveIframeProps { export function ResponsiveIframe({ src, className = '', allow, style, token }: ResponsiveIframeProps) { const iframeRef = useRef(null); - + const { data: session } = useSession(); + const [authError, setAuthError] = useState(null); + + // Add token parameter only if token is provided const fullSrc = token ? `${src}${src.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}` : src; + // Handle silent authentication refresh + useEffect(() => { + let silentRefreshTimer: NodeJS.Timeout; + + // Set up periodic silent refresh (every 15 minutes) + const startSilentRefresh = () => { + silentRefreshTimer = setInterval(() => { + console.log('Performing silent authentication check for iframes'); + + // Create a hidden iframe for silent authentication + const refreshFrame = document.createElement('iframe'); + refreshFrame.style.display = 'none'; + refreshFrame.src = '/silent-refresh'; + document.body.appendChild(refreshFrame); + + // Remove iframe after it has loaded (5 seconds timeout) + setTimeout(() => { + if (refreshFrame && refreshFrame.parentNode) { + refreshFrame.parentNode.removeChild(refreshFrame); + } + }, 5000); + }, 15 * 60 * 1000); // 15 minutes + }; + + if (session) { + startSilentRefresh(); + } + + return () => { + if (silentRefreshTimer) { + clearInterval(silentRefreshTimer); + } + }; + }, [session]); + useEffect(() => { const iframe = iframeRef.current; if (!iframe) return; @@ -44,16 +83,25 @@ export function ResponsiveIframe({ src, className = '', allow, style, token }: R // Handle authentication messages from iframe const handleMessage = (event: MessageEvent) => { - // Only accept messages from our iframe - if (iframe.contentWindow !== event.source) return; + // Accept messages from our iframe or from silent auth iframe + if (event.source !== iframe.contentWindow && + !event.data?.type?.startsWith('SILENT_AUTH_')) return; - const { type, data } = event.data || {}; + const { type, data, error } = event.data || {}; - // Handle auth related messages + // Handle auth-related messages if (type === 'AUTH_ERROR' || type === 'SESSION_EXPIRED') { - console.log('Auth error in iframe:', data); - // Optionally redirect to login page - // window.location.href = '/signin'; + console.log('Auth error in iframe:', data || error); + setAuthError(error || 'Authentication error'); + } else if (type === 'SILENT_AUTH_SUCCESS') { + console.log('Silent authentication successful'); + setAuthError(null); + } else if (type === 'SILENT_AUTH_FAILURE') { + console.log('Silent authentication failed:', error); + // Only set error if it's persistent + if (error !== 'loading') { + setAuthError('Session expired'); + } } }; @@ -77,19 +125,33 @@ export function ResponsiveIframe({ src, className = '', allow, style, token }: R }, []); return ( -