courrier msft oauth

This commit is contained in:
alma 2025-05-02 10:02:58 +02:00
parent dcad2f160d
commit 92e0c0dd3b
3 changed files with 98 additions and 52 deletions

View File

@ -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,

View File

@ -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`);
}

View File

@ -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 };
}
}