From 6c94b7f8f859431153a1625d56346eddc2caeb60 Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 27 Apr 2025 13:52:04 +0200 Subject: [PATCH] courrier redis login --- .env | 3 + app/api/courrier/login/route.ts | 7 ++ app/api/courrier/session/route.ts | 64 +++++++++++++++++ app/courrier/page.tsx | 38 +++++++++- lib/services/prefetch-service.ts | 111 ++++++++++++++++++++++++++++++ scripts/test-redis-env.js | 45 ++++++++++++ 6 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 app/api/courrier/session/route.ts create mode 100644 lib/services/prefetch-service.ts create mode 100644 scripts/test-redis-env.js diff --git a/.env b/.env index d2ae9e7f..36f3843e 100644 --- a/.env +++ b/.env @@ -77,3 +77,6 @@ IMAP_HOST=mail.infomaniak.com IMAP_PORT=993 NEWS_API_URL="http://172.16.0.104:8000" +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD=mySecretPassword diff --git a/app/api/courrier/login/route.ts b/app/api/courrier/login/route.ts index b7e2ce98..b36bc16b 100644 --- a/app/api/courrier/login/route.ts +++ b/app/api/courrier/login/route.ts @@ -6,6 +6,7 @@ import { getUserEmailCredentials, testEmailConnection } from '@/lib/services/email-service'; +import { prefetchUserEmailData } from '@/lib/services/prefetch-service'; import { cacheEmailCredentials, invalidateUserEmailCache } from '@/lib/redis'; export async function POST(request: Request) { @@ -56,6 +57,12 @@ export async function POST(request: Request) { port: parseInt(port) }); + // Start prefetching email data in the background + // We don't await this to avoid blocking the response + prefetchUserEmailData(session.user.id).catch(err => { + console.error('Background prefetch error:', err); + }); + return NextResponse.json({ success: true }); } catch (error) { console.error('Error in login handler:', error); diff --git a/app/api/courrier/session/route.ts b/app/api/courrier/session/route.ts new file mode 100644 index 00000000..f58d3d30 --- /dev/null +++ b/app/api/courrier/session/route.ts @@ -0,0 +1,64 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { getUserEmailCredentials } from '@/lib/services/email-service'; +import { prefetchUserEmailData } from '@/lib/services/prefetch-service'; +import { getCachedEmailCredentials } from '@/lib/redis'; + +/** + * This endpoint is called when the app initializes to check if the user has email credentials + * and to start prefetching email data in the background if they do + */ +export async function GET() { + try { + // Get server session to verify authentication + const session = await getServerSession(authOptions); + + // Check if user is authenticated + if (!session?.user?.id) { + return NextResponse.json({ + authenticated: false, + message: "Not authenticated" + }); + } + + const userId = session.user.id; + + // First, check Redis cache for credentials + let credentials = await getCachedEmailCredentials(userId); + + // If not in cache, check database + if (!credentials) { + credentials = await getUserEmailCredentials(userId); + } + + // If no credentials found + if (!credentials) { + return NextResponse.json({ + authenticated: true, + hasEmailCredentials: false, + message: "No email credentials found" + }); + } + + // Start prefetching email data in the background + // We don't await this to avoid blocking the response + prefetchUserEmailData(userId).catch(err => { + console.error('Background prefetch error:', err); + }); + + // Return session info without sensitive data + return NextResponse.json({ + authenticated: true, + hasEmailCredentials: true, + email: credentials.email, + prefetchStarted: true + }); + } catch (error) { + console.error("Error checking session:", error); + return NextResponse.json({ + authenticated: false, + error: "Internal Server Error" + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index b7645cf5..8f38ef57 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { useSession } from 'next-auth/react'; import { Mail, Loader2, AlertCircle, MoreVertical, Settings, Plus as PlusIcon, Trash2, Edit, @@ -37,6 +38,9 @@ import { DeleteConfirmDialog, LoginNeededAlert } from '@/components/email/EmailD // Import the custom hook import { useCourrier, EmailData } from '@/hooks/use-courrier'; +// Import the prefetching function +import { prefetchFolderEmails } from '@/lib/services/prefetch-service'; + // Simplified version for this component function SimplifiedLoadingFix() { // In production, don't render anything @@ -62,6 +66,7 @@ interface Account { export default function CourrierPage() { const router = useRouter(); + const { data: session } = useSession(); // Get all the email functionality from the hook const { @@ -101,6 +106,7 @@ export default function CourrierPage() { const [currentView, setCurrentView] = useState('INBOX'); const [unreadCount, setUnreadCount] = useState(0); const [loading, setLoading] = useState(false); + const [prefetchStarted, setPrefetchStarted] = useState(false); // Mock accounts for the sidebar const [accounts, setAccounts] = useState([ @@ -127,6 +133,29 @@ export default function CourrierPage() { setUnreadCount(unreadInInbox); }, [emails, currentFolder]); + // Initialize session and start prefetching + useEffect(() => { + const initSession = async () => { + try { + // Call the session API to check email credentials and start prefetching + const response = await fetch('/api/courrier/session'); + const data = await response.json(); + + if (data.authenticated && data.hasEmailCredentials) { + console.log('Session initialized, prefetching started'); + setPrefetchStarted(true); + } else if (data.authenticated && !data.hasEmailCredentials) { + // User is authenticated but doesn't have email credentials + setShowLoginNeeded(true); + } + } catch (error) { + console.error('Error initializing session:', error); + } + }; + + initSession(); + }, []); + // Helper to get folder icons const getFolderIcon = (folder: string) => { const folderLower = folder.toLowerCase(); @@ -216,10 +245,17 @@ export default function CourrierPage() { setShowComposeModal(true); }; - // Handle mailbox change + // Handle mailbox change with prefetching const handleMailboxChange = (folder: string) => { changeFolder(folder); setCurrentView(folder); + + // Start prefetching additional pages for this folder + if (session?.user?.id && folder) { + prefetchFolderEmails(session.user.id, folder, 3).catch(err => { + console.error(`Error prefetching ${folder}:`, err); + }); + } }; // Handle sending email diff --git a/lib/services/prefetch-service.ts b/lib/services/prefetch-service.ts new file mode 100644 index 00000000..36657c5d --- /dev/null +++ b/lib/services/prefetch-service.ts @@ -0,0 +1,111 @@ +'use server'; + +import { getImapConnection, getEmails, getEmailContent } from './email-service'; +import { + cacheEmailList, + cacheEmailContent, + cacheImapSession +} from '@/lib/redis'; + +/** + * Prefetch basic email data for faster initial loading + * This function should be called when a user logs in + */ +export async function prefetchUserEmailData(userId: string): Promise { + console.log(`Starting email prefetch for user ${userId}`); + const startTime = Date.now(); + + try { + // Connect to IMAP server + const client = await getImapConnection(userId); + + // 1. Prefetch mailbox list + const mailboxes = await client.list(); + const mailboxPaths = mailboxes.map(mailbox => mailbox.path); + + // Cache mailbox list in session data + await cacheImapSession(userId, { + lastActive: Date.now(), + mailboxes: mailboxPaths + }); + + console.log(`Prefetched ${mailboxPaths.length} folders for user ${userId}`); + + // 2. Prefetch email lists for important folders + const importantFolders = [ + 'INBOX', + mailboxPaths.find(path => path.toLowerCase().includes('sent')) || 'Sent', + mailboxPaths.find(path => path.toLowerCase().includes('draft')) || 'Drafts' + ].filter(Boolean); + + // Fetch first page of each important folder + for (const folder of importantFolders) { + try { + console.log(`Prefetching emails for ${folder}`); + const emailList = await getEmails(userId, folder, 1, 20); + console.log(`Prefetched ${emailList.emails.length} emails for ${folder}`); + } catch (error) { + console.error(`Error prefetching emails for folder ${folder}:`, error); + // Continue with other folders even if one fails + } + } + + // 3. Prefetch content of recent unread emails in INBOX + try { + // Get the list again (it's already cached so this will be fast) + const inboxList = await getEmails(userId, 'INBOX', 1, 20); + + // Prefetch content for up to 5 recent unread emails + const unreadEmails = inboxList.emails + .filter(email => !email.flags.seen) + .slice(0, 5); + + if (unreadEmails.length > 0) { + console.log(`Prefetching content for ${unreadEmails.length} unread emails`); + + // Fetch content in parallel for speed + await Promise.allSettled( + unreadEmails.map(email => + getEmailContent(userId, email.id, 'INBOX') + .catch(err => console.error(`Error prefetching email ${email.id}:`, err)) + ) + ); + + console.log(`Completed prefetching content for unread emails`); + } + } catch (error) { + console.error('Error prefetching unread email content:', error); + } + + const duration = (Date.now() - startTime) / 1000; + console.log(`Email prefetch completed for user ${userId} in ${duration.toFixed(2)}s`); + } catch (error) { + console.error('Error during email prefetch:', error); + } +} + +/** + * Prefetch a specific folder's emails + * This can be used when the user navigates to a folder to preload more pages + */ +export async function prefetchFolderEmails( + userId: string, + folder: string, + pages: number = 3 +): Promise { + try { + console.log(`Prefetching ${pages} pages of emails for folder ${folder}`); + + // Fetch multiple pages in parallel + await Promise.allSettled( + Array.from({ length: pages }, (_, i) => i + 1).map(page => + getEmails(userId, folder, page, 20) + .catch(err => console.error(`Error prefetching page ${page} of ${folder}:`, err)) + ) + ); + + console.log(`Completed prefetching ${pages} pages of ${folder}`); + } catch (error) { + console.error(`Error prefetching folder ${folder}:`, error); + } +} \ No newline at end of file diff --git a/scripts/test-redis-env.js b/scripts/test-redis-env.js new file mode 100644 index 00000000..aa715dc5 --- /dev/null +++ b/scripts/test-redis-env.js @@ -0,0 +1,45 @@ +#!/usr/bin/env node +require('dotenv').config(); + +const Redis = require('ioredis'); + +console.log('Redis configuration from environment:'); +console.log('- Host:', process.env.REDIS_HOST); +console.log('- Port:', process.env.REDIS_PORT); +console.log('- Password:', process.env.REDIS_PASSWORD ? '******** (set)' : '(not set)'); + +const redis = new Redis({ + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined, + password: process.env.REDIS_PASSWORD, + maxRetriesPerRequest: 3, + retryStrategy: (times) => Math.min(times * 100, 3000) +}); + +redis.on('connect', () => { + console.log('✅ Connected to Redis successfully!'); + + // Test a simple operation + redis.set('test-key', 'Test value from environment test') + .then(() => redis.get('test-key')) + .then((value) => { + console.log('✅ Successfully set and retrieved a test key:', value); + redis.quit(); + }) + .catch((err) => { + console.error('❌ Error during Redis operations:', err); + redis.quit(); + process.exit(1); + }); +}); + +redis.on('error', (err) => { + console.error('❌ Redis connection error:', err); + process.exit(1); +}); + +// Add a timeout to avoid hanging indefinitely +setTimeout(() => { + console.error('❌ Connection timeout'); + process.exit(1); +}, 5000); \ No newline at end of file