NeahNew/lib/services/token-refresh.ts
2026-01-01 19:31:48 +01:00

124 lines
4.4 KiB
TypeScript

import { refreshAccessToken } from './microsoft-oauth';
import { getRedisClient, KEYS } from '@/lib/redis';
import { prisma } from '@/lib/prisma';
/**
* 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)
console.log(`Checking if token refresh is needed for ${email}`);
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
console.log(`No credentials found in Redis for ${email}, checking Prisma database...`);
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);
console.log(`Recovered credentials from Prisma and cached in Redis for ${email}`);
} else {
console.log(`No OAuth credentials found in database for ${email}`);
return { accessToken: '', success: false };
}
}
// 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`);
// 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
}
});
console.log(`Token for ${email} persisted to Prisma database`);
} else {
console.warn(`Account ${email} not found in Prisma, cannot persist tokens`);
}
} catch (dbError) {
console.error(`Error persisting tokens to database for ${email}:`, dbError);
// Don't fail the refresh if DB update fails - Redis cache is still updated
}
return { accessToken: tokens.access_token, success: true };
} catch (error) {
console.error(`Error refreshing token for ${email}:`, error);
return { accessToken: '', success: false };
}
}