diff --git a/app/api/courrier/microsoft/callback/route.ts b/app/api/courrier/microsoft/callback/route.ts index 87f19848..4f9533c5 100644 --- a/app/api/courrier/microsoft/callback/route.ts +++ b/app/api/courrier/microsoft/callback/route.ts @@ -5,6 +5,7 @@ import { exchangeCodeForTokens } from '@/lib/services/microsoft-oauth'; import { prisma } from '@/lib/prisma'; import { testEmailConnection, saveUserEmailCredentials } from '@/lib/services/email-service'; import { invalidateFolderCache } from '@/lib/redis'; +import { cacheEmailCredentials } from '@/lib/redis'; export async function POST(request: Request) { try { @@ -66,8 +67,8 @@ export async function POST(request: Request) { // Create credentials object for Microsoft account const credentials = { email: userEmail, - // Password is empty for OAuth accounts - password: '', + // Password is empty for OAuth accounts - use a placeholder to meet database schema requirements + password: 'microsoft-oauth2-account', // Use Microsoft's IMAP server for Outlook/Office365 host: 'outlook.office365.com', port: 993, @@ -127,6 +128,9 @@ export async function POST(request: Request) { // Invalidate any existing folder caches await invalidateFolderCache(session.user.id, userEmail, '*'); + // First cache the credentials in Redis to ensure OAuth data is saved + await cacheEmailCredentials(session.user.id, userEmail, credentials); + return NextResponse.json({ success: true, account: createdAccount, diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 6e92c9fb..abaa0b57 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -255,6 +255,13 @@ export async function getImapConnection( // First try to get credentials from Redis cache let credentials = await getCachedEmailCredentials(userId, accountId); + console.log(`Retrieved credentials from Redis cache for ${userId}:${accountId}:`, credentials ? { + email: credentials.email, + hasPassword: !!credentials.password, + useOAuth: !!credentials.useOAuth, + hasAccessToken: !!credentials.accessToken, + hasRefreshToken: !!credentials.refreshToken + } : 'No credentials found in cache'); // If not in cache, get from database and cache them if (!credentials) { @@ -276,6 +283,12 @@ export async function getImapConnection( throw new Error('Email account credentials not found'); } + console.log(`Database lookup returned credentials for ${dbCredentials.email}:`, { + email: dbCredentials.email, + hasPassword: !!dbCredentials.password, + fields: Object.keys(dbCredentials) + }); + // Create our credentials object from database data credentials = { email: dbCredentials.email, @@ -294,7 +307,34 @@ export async function getImapConnection( // Cast to extended type const extendedCreds = credentials as EmailCredentialsExtended; - // If this account is marked as using OAuth in the cache + // MICROSOFT FIX: Detect Microsoft accounts by hostname and set OAuth flag + if (extendedCreds.host === 'outlook.office365.com') { + console.log(`Microsoft account detected (${extendedCreds.email}), setting useOAuth=true`); + extendedCreds.useOAuth = true; + + // If we have no password but useOAuth is true, we need to make sure refresh token exists in Redis + if (!extendedCreds.password && !extendedCreds.accessToken) { + // If running in browser edge environment (serverless), try to refresh our tokens from Redis + try { + const cachedCreds = await getCachedEmailCredentials(userId, accountId); + if (cachedCreds && cachedCreds.refreshToken) { + console.log(`Found refresh token in Redis for ${extendedCreds.email}, will use it`); + extendedCreds.refreshToken = cachedCreds.refreshToken; + extendedCreds.accessToken = cachedCreds.accessToken; + extendedCreds.tokenExpiry = cachedCreds.tokenExpiry; + + // Make sure we cache these credentials again with the tokens + await cacheEmailCredentials(userId, accountId, extendedCreds); + } else { + console.warn(`No refresh token found for ${extendedCreds.email} in Redis cache`); + } + } catch (err) { + console.error(`Error retrieving cached credentials for ${extendedCreds.email}:`, err); + } + } + } + + // If using OAuth, ensure we have a fresh token if (extendedCreds.useOAuth) { console.log(`Account is configured to use OAuth`); @@ -384,6 +424,17 @@ async function createImapConnection(credentials: EmailCredentials, connectionKey // Cast to extended type const extendedCreds = credentials as EmailCredentialsExtended; + console.log(`Creating IMAP connection with credentials:`, { + email: extendedCreds.email, + host: extendedCreds.host, + port: extendedCreds.port, + hasPassword: !!extendedCreds.password, + useOAuth: !!extendedCreds.useOAuth, + hasAccessToken: !!extendedCreds.accessToken, + hasRefreshToken: !!extendedCreds.refreshToken, + hasTokenExpiry: !!extendedCreds.tokenExpiry + }); + let authParams: any; // Check if we have valid OAuth tokens @@ -406,6 +457,11 @@ async function createImapConnection(credentials: EmailCredentials, connectionKey }; } else { // No authentication method available + console.error(`No authentication method found for ${connectionKey}:`, { + hasPassword: !!extendedCreds.password, + useOAuth: !!extendedCreds.useOAuth, + hasAccessToken: !!extendedCreds.accessToken + }); throw new Error(`No authentication method available for ${connectionKey} - need either password or OAuth token`); } diff --git a/lib/services/token-refresh.ts b/lib/services/token-refresh.ts index 68449bdb..957b7b4f 100644 --- a/lib/services/token-refresh.ts +++ b/lib/services/token-refresh.ts @@ -1,5 +1,4 @@ import { refreshAccessToken } from './microsoft-oauth'; -import { prisma } from '@/lib/prisma'; import { getRedisClient, KEYS } from '@/lib/redis'; /** @@ -18,62 +17,49 @@ export async function ensureFreshToken( email: string ): Promise<{ accessToken: string; success: boolean }> { try { - // Get stored credentials using raw query until Prisma schema is updated - const credentials = await prisma.$queryRaw` - SELECT "useOAuth", "refreshToken", "accessToken", "tokenExpiry" - FROM "MailCredentials" - WHERE "userId" = ${userId} AND "email" = ${email} - LIMIT 1 - `; - - const credData = Array.isArray(credentials) && credentials.length > 0 - ? credentials[0] - : null; - - // If not OAuth or missing refresh token, return failure - if (!credData?.useOAuth || !credData.refreshToken) { - return { accessToken: '', success: false }; - } - - // If token is still valid, return current token - if (credData.tokenExpiry && credData.accessToken && - new Date(credData.tokenExpiry) > new Date(Date.now() + 5 * 60 * 1000)) { - return { accessToken: credData.accessToken, success: true }; - } - - // Token is expired or about to expire, refresh it - console.log(`Refreshing token for user ${userId}, account ${email}`); - const tokens = await refreshAccessToken(credData.refreshToken); - - // Update database with new token information using raw query - await prisma.$executeRaw` - UPDATE "MailCredentials" - SET - "accessToken" = ${tokens.access_token}, - "refreshToken" = ${tokens.refresh_token || credData.refreshToken}, - "tokenExpiry" = ${new Date(Date.now() + (tokens.expires_in * 1000))} - WHERE "userId" = ${userId} AND "email" = ${email} - `; - - // Update Redis cache + // Use Redis to get the tokens (no database lookup needed) + console.log(`Checking if token refresh is needed for ${email}`); const redis = getRedisClient(); const key = KEYS.CREDENTIALS(userId, email); const credStr = await redis.get(key); - if (credStr) { - const creds = JSON.parse(credStr); - creds.accessToken = tokens.access_token; - if (tokens.refresh_token) { - creds.refreshToken = tokens.refresh_token; - } - creds.tokenExpiry = Date.now() + (tokens.expires_in * 1000); - - await redis.set(key, JSON.stringify(creds), 'EX', 86400); // 24 hours + if (!credStr) { + console.log(`No credentials found in Redis for ${email}`); + return { accessToken: '', success: false }; } + + const creds = JSON.parse(credStr); + + // If not OAuth or missing refresh token, return failure + if (!creds.useOAuth || !creds.refreshToken) { + console.log(`Account ${email} is not using OAuth or missing refresh token`); + return { accessToken: '', success: false }; + } + + // If token is still valid, return current token + if (creds.tokenExpiry && creds.accessToken && + creds.tokenExpiry > Date.now() + 5 * 60 * 1000) { + console.log(`Token for ${email} is still valid, no refresh needed`); + return { accessToken: creds.accessToken, success: true }; + } + + // Token is expired or about to expire, refresh it + console.log(`Refreshing token for ${email}`); + const tokens = await refreshAccessToken(creds.refreshToken); + + // Update Redis cache with new tokens + creds.accessToken = tokens.access_token; + if (tokens.refresh_token) { + creds.refreshToken = tokens.refresh_token; + } + creds.tokenExpiry = Date.now() + (tokens.expires_in * 1000); + + await redis.set(key, JSON.stringify(creds), 'EX', 86400); // 24 hours + console.log(`Token for ${email} refreshed and cached in Redis`); return { accessToken: tokens.access_token, success: true }; } catch (error) { - console.error(`Error refreshing token for user ${userId}:`, error); + console.error(`Error refreshing token for ${email}:`, error); return { accessToken: '', success: false }; } } \ No newline at end of file