diff --git a/app/api/courrier/login/route.ts b/app/api/courrier/login/route.ts index b7c02ac5..34706a67 100644 --- a/app/api/courrier/login/route.ts +++ b/app/api/courrier/login/route.ts @@ -7,7 +7,12 @@ import { testEmailConnection } from '@/lib/services/email-service'; import { prefetchUserEmailData } from '@/lib/services/prefetch-service'; -import { cacheEmailCredentials, invalidateUserEmailCache } from '@/lib/redis'; +import { + cacheEmailCredentials, + invalidateUserEmailCache, + getCachedEmailCredentials +} from '@/lib/redis'; +import { prisma } from '@/lib/prisma'; export async function POST(request: Request) { try { @@ -49,13 +54,18 @@ export async function POST(request: Request) { // Invalidate all cached data for this user as they are changing their credentials await invalidateUserEmailCache(session.user.id); - // Save credentials in the database and Redis - await saveUserEmailCredentials(session.user.id, email, { + // Create credentials object with required fields + const credentials = { email, password, host, - port: parseInt(port) - }); + port: parseInt(port), + secure: true // Default to secure connection + }; + + // Save credentials in the database and Redis + // Use email as the accountId since it's unique per user + await saveUserEmailCredentials(session.user.id, email, credentials); // Start prefetching email data in the background // We don't await this to avoid blocking the response @@ -67,10 +77,7 @@ export async function POST(request: Request) { } catch (error) { console.error('Error in login handler:', error); return NextResponse.json( - { - error: 'An unexpected error occurred', - details: error instanceof Error ? error.message : 'Unknown error' - }, + { error: 'An unexpected error occurred' }, { status: 500 } ); } @@ -78,7 +85,6 @@ export async function POST(request: Request) { export async function GET() { try { - // Authenticate user const session = await getServerSession(authOptions); if (!session?.user?.id) { return NextResponse.json( @@ -87,8 +93,26 @@ export async function GET() { ); } - // Get user credentials from database - const credentials = await getUserEmailCredentials(session.user.id); + // First try to get from Redis cache + let credentials = await getCachedEmailCredentials(session.user.id, 'default'); + + // If not in cache, get from database + if (!credentials) { + credentials = await prisma.mailCredentials.findUnique({ + where: { + userId: session.user.id + }, + select: { + email: true, + host: true, + port: true + } + }); + } else { + // Remove password from response + const { password, ...safeCredentials } = credentials; + credentials = safeCredentials; + } if (!credentials) { return NextResponse.json( @@ -97,14 +121,8 @@ export async function GET() { ); } - // Return credentials without the password - return NextResponse.json({ - email: credentials.email, - host: credentials.host, - port: credentials.port - }); + return NextResponse.json(credentials); } catch (error) { - console.error('Error fetching credentials:', error); return NextResponse.json( { error: 'Failed to retrieve credentials' }, { status: 500 } diff --git a/app/api/courrier/session/route.ts b/app/api/courrier/session/route.ts index ace33571..0ca813d8 100644 --- a/app/api/courrier/session/route.ts +++ b/app/api/courrier/session/route.ts @@ -4,9 +4,26 @@ import { authOptions } from '@/lib/auth'; import { getMailboxes } from '@/lib/services/email-service'; import { getRedisClient } from '@/lib/redis'; import { getImapConnection } from '@/lib/services/email-service'; -import { MailCredentials } from '@prisma/client'; import { prisma } from '@/lib/prisma'; +// Define extended MailCredentials type +interface MailCredentials { + id: string; + userId: string; + email: string; + password: string; + host: string; + port: number; + secure?: boolean; + smtp_host?: string | null; + smtp_port?: number | null; + smtp_secure?: boolean | null; + display_name?: string | null; + color?: string | null; + createdAt: Date; + updatedAt: Date; +} + // Keep track of last prefetch time for each user const lastPrefetchMap = new Map(); const PREFETCH_COOLDOWN_MS = 30000; // 30 seconds cooldown between prefetches @@ -23,23 +40,39 @@ const FOLDERS_CACHE_KEY = (userId: string, accountId: string) => `email:folders: */ export async function GET() { try { + // Get Redis connection first to ensure it's available + const redis = getRedisClient(); + if (!redis) { + console.error('Redis connection failed'); + return NextResponse.json({ error: 'Redis connection failed' }, { status: 500 }); + } + // Get session with detailed logging console.log('Attempting to get server session...'); const session = await getServerSession(authOptions); if (!session) { console.error('No session found'); - return NextResponse.json({ error: 'No session found' }, { status: 401 }); + return NextResponse.json({ + authenticated: false, + error: 'No session found' + }, { status: 401 }); } if (!session.user) { console.error('No user in session'); - return NextResponse.json({ error: 'No user in session' }, { status: 401 }); + return NextResponse.json({ + authenticated: false, + error: 'No user in session' + }, { status: 401 }); } if (!session.user.id) { console.error('No user ID in session'); - return NextResponse.json({ error: 'No user ID in session' }, { status: 401 }); + return NextResponse.json({ + authenticated: false, + error: 'No user ID in session' + }, { status: 401 }); } console.log('Session validated successfully:', { @@ -48,13 +81,6 @@ export async function GET() { name: session.user.name }); - // Get Redis connection - const redis = getRedisClient(); - if (!redis) { - console.error('Redis connection failed'); - return NextResponse.json({ error: 'Redis connection failed' }, { status: 500 }); - } - // Get user with their accounts console.log('Fetching user with ID:', session.user.id); const user = await prisma.user.findUnique({ @@ -64,7 +90,11 @@ export async function GET() { if (!user) { console.error('User not found in database'); - return NextResponse.json({ error: 'User not found' }, { status: 404 }); + return NextResponse.json({ + authenticated: true, + hasEmailCredentials: false, + error: 'User not found in database' + }); } // Get all accounts for the user @@ -73,6 +103,7 @@ export async function GET() { console.log('No email accounts found for user:', session.user.id); return NextResponse.json({ authenticated: true, + hasEmailCredentials: false, accounts: [], message: 'No email accounts found' }); @@ -84,30 +115,38 @@ export async function GET() { const accountsWithFolders = await Promise.all( accounts.map(async (account: MailCredentials) => { const cacheKey = FOLDERS_CACHE_KEY(user.id, account.id); - // Try to get folders from Redis cache first - const cachedFolders = await redis.get(cacheKey); - if (cachedFolders) { - console.log(`Using cached folders for account ${account.email}`); - return { - ...account, - folders: JSON.parse(cachedFolders) - }; - } - - // If not in cache, fetch from IMAP - console.log(`Fetching folders from IMAP for account ${account.email}`); - const client = await getImapConnection(user.id, account.id); - if (!client) { - console.warn(`Failed to get IMAP connection for account ${account.email}`); - return { - ...account, - folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'] - }; - } - + try { + // Try to get folders from Redis cache first + const cachedFolders = await redis.get(cacheKey); + if (cachedFolders) { + console.log(`Using cached folders for account ${account.email}`); + return { + id: account.id, + email: account.email, + display_name: account.display_name, + color: account.color, + folders: JSON.parse(cachedFolders) + }; + } + + // If not in cache, fetch from IMAP + console.log(`Fetching folders from IMAP for account ${account.email}`); + const client = await getImapConnection(user.id, account.id); + if (!client) { + console.warn(`Failed to get IMAP connection for account ${account.email}`); + return { + id: account.id, + email: account.email, + display_name: account.display_name, + color: account.color, + folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'] + }; + } + const folders = await getMailboxes(client); console.log(`Fetched ${folders.length} folders for account ${account.email}`); + // Cache the folders in Redis await redis.set( cacheKey, @@ -115,14 +154,21 @@ export async function GET() { 'EX', FOLDERS_CACHE_TTL ); + return { - ...account, + id: account.id, + email: account.email, + display_name: account.display_name, + color: account.color, folders }; } catch (error) { console.error(`Error fetching folders for account ${account.id}:`, error); return { - ...account, + id: account.id, + email: account.email, + display_name: account.display_name, + color: account.color, folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'] }; } @@ -131,12 +177,17 @@ export async function GET() { return NextResponse.json({ authenticated: true, + hasEmailCredentials: true, accounts: accountsWithFolders }); } catch (error) { console.error('Error in session route:', error); return NextResponse.json( - { error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' }, + { + authenticated: false, + error: 'Internal server error', + details: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 } ); } diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index f527fb3f..a49b8ef2 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -156,7 +156,12 @@ export async function getImapConnection( */ export async function getUserEmailCredentials(userId: string, accountId?: string): Promise { const credentials = await prisma.mailCredentials.findFirst({ - where: accountId ? { userId, id: accountId } : { userId } + where: { + AND: [ + { userId }, + { email: accountId } + ] + } }); if (!credentials) return null; @@ -197,7 +202,6 @@ export async function saveUserEmailCredentials( credentials: EmailCredentials ): Promise { console.log('Saving credentials for user:', userId, 'account:', accountId); - console.log('Saving credentials for user:', userId, 'account:', credentials); if (!credentials) { throw new Error('No credentials provided'); @@ -217,22 +221,34 @@ export async function saveUserEmailCredentials( color: credentials.color || null }; - // Save to database - await prisma.mailCredentials.upsert({ - where: { - id: accountId, - userId - }, - update: dbCredentials, - create: { - id: accountId, - userId, - ...dbCredentials - } - }); - - // Cache the full credentials object in Redis (with all fields) - await cacheEmailCredentials(userId, accountId, credentials); + try { + // Save to database using the unique constraint on [userId, email] + await prisma.mailCredentials.upsert({ + where: { + id: await prisma.mailCredentials.findFirst({ + where: { + AND: [ + { userId }, + { email: accountId } + ] + }, + select: { id: true } + }).then(result => result?.id ?? '') + }, + update: dbCredentials, + create: { + userId, + ...dbCredentials + } + }); + + // Cache the full credentials object in Redis + await cacheEmailCredentials(userId, accountId, credentials); + console.log('Successfully saved and cached credentials for user:', userId); + } catch (error) { + console.error('Error saving credentials:', error); + throw error; + } } // Helper type for IMAP fetch options @@ -274,10 +290,12 @@ export async function getEmails( if (accountId) { try { // Get account from database - const account = await prisma.mailCredentials.findUnique({ - where: { - id: accountId, - userId + const account = await prisma.mailCredentials.findFirst({ + where: { + AND: [ + { userId }, + { email: accountId } + ] } });