import { refreshAccessToken } from './microsoft-oauth'; import { getRedisClient, KEYS } from '@/lib/redis'; import { prisma } from '@/lib/prisma'; import { logger } from '@/lib/logger'; /** * Check if a token is expired or about to expire (within 5 minutes) */ export function isTokenExpired(expiryTimestamp: number): boolean { const fiveMinutesInMs = 5 * 60 * 1000; return Date.now() + fiveMinutesInMs >= expiryTimestamp; } /** * Refresh an access token if it's expired or about to expire */ export async function ensureFreshToken( userId: string, email: string ): Promise<{ accessToken: string; success: boolean }> { try { // Try Redis first (fast path) logger.debug('[TOKEN_REFRESH] Checking if token refresh is needed', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); const redis = getRedisClient(); const key = KEYS.CREDENTIALS(userId, email); let credStr = await redis.get(key); let creds: any = null; if (credStr) { creds = JSON.parse(credStr); } else { // Redis cache miss - fallback to Prisma database logger.debug('[TOKEN_REFRESH] No credentials in Redis, checking Prisma', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); const account = await prisma.mailCredentials.findFirst({ where: { userId: userId, email: email, use_oauth: true } }); if (account && account.refresh_token) { // Reconstruct credentials from database creds = { useOAuth: true, refreshToken: account.refresh_token, accessToken: account.access_token || null, tokenExpiry: account.token_expiry ? account.token_expiry.getTime() : null, email: account.email, host: account.host, port: account.port, secure: account.secure }; // Re-populate Redis cache await redis.set(key, JSON.stringify(creds), 'EX', 86400); logger.debug('[TOKEN_REFRESH] Recovered credentials from Prisma and cached in Redis', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); } else { logger.debug('[TOKEN_REFRESH] No OAuth credentials found in database', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); return { accessToken: '', success: false }; } } // If not OAuth or missing refresh token, return failure if (!creds.useOAuth || !creds.refreshToken) { logger.debug('[TOKEN_REFRESH] Account not using OAuth or missing refresh token', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); return { accessToken: '', success: false }; } // If token is still valid, return current token if (creds.tokenExpiry && creds.accessToken && creds.tokenExpiry > Date.now() + 5 * 60 * 1000) { logger.debug('[TOKEN_REFRESH] Token still valid, no refresh needed', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); return { accessToken: creds.accessToken, success: true }; } // Token is expired or about to expire, refresh it logger.debug('[TOKEN_REFRESH] Refreshing token', { emailHash: Buffer.from(email.toLowerCase()).toString('base64').slice(0, 12), }); 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 logger.debug('[TOKEN_REFRESH] Token refreshed and cached in Redis', { email: email.substring(0, 5) + '***' }); // CRITICAL: Also persist to Prisma database for long-term storage // This ensures refresh tokens survive Redis restarts/expiry try { const account = await prisma.mailCredentials.findFirst({ where: { userId: userId, email: email } }); if (account) { await prisma.mailCredentials.update({ where: { id: account.id }, data: { access_token: tokens.access_token, refresh_token: tokens.refresh_token || account.refresh_token, // Keep existing if not provided token_expiry: new Date(Date.now() + (tokens.expires_in * 1000)), use_oauth: true } }); logger.debug('[TOKEN_REFRESH] Token persisted to Prisma database', { email: email.substring(0, 5) + '***' }); } else { logger.warn('[TOKEN_REFRESH] Account not found in Prisma, cannot persist tokens', { email: email.substring(0, 5) + '***' }); } } catch (dbError) { logger.error('[TOKEN_REFRESH] Error persisting tokens to database', { email: email.substring(0, 5) + '***', error: dbError instanceof Error ? dbError.message : String(dbError) }); // Don't fail the refresh if DB update fails - Redis cache is still updated } return { accessToken: tokens.access_token, success: true }; } catch (error) { logger.error('[TOKEN_REFRESH] Error refreshing token', { email: email.substring(0, 5) + '***', error: error instanceof Error ? error.message : String(error) }); return { accessToken: '', success: false }; } }