145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
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 };
|
|
}
|
|
}
|