Fondation

This commit is contained in:
alma 2026-01-16 21:33:36 +01:00
parent f645103946
commit 2213b6001c
16 changed files with 395 additions and 135 deletions

70
CLEANUP_SUMMARY.md Normal file
View File

@ -0,0 +1,70 @@
# Résumé du Nettoyage - console.log et fetch()
## ✅ Fichiers Nettoyés
### Services Backend (100% nettoyés)
- ✅ `lib/services/n8n-service.ts` - 3 fetch() → fetchWithTimeout()
- ✅ `lib/services/rocketchat-call-listener.ts` - 35 console.log → logger
- ✅ `lib/services/microsoft-oauth.ts` - 12 console.log → logger
- ✅ `lib/services/token-refresh.ts` - 12 console.log → logger
- ✅ `lib/services/refresh-manager.ts` - 19 console.log → logger
- ✅ `lib/services/prefetch-service.ts` - 18 console.log → logger
- ✅ `lib/services/caldav-sync.ts` - 12 console.log → logger
- ✅ `lib/services/email-service.ts` - 2 console.error → logger
### Routes API Critiques (100% nettoyées)
- ✅ `app/api/missions/[missionId]/generate-plan/route.ts` - 1 fetch() → fetchWithTimeout()
- ✅ `app/api/users/[userId]/route.ts` - 5 fetch() → fetchWithTimeout(), tous console.log → logger
- ✅ `app/api/rocket-chat/messages/route.ts` - 5 fetch() → fetchWithTimeout(), tous console.log → logger
- ✅ `app/api/leantime/tasks/route.ts` - 2 fetch() → fetchWithTimeout()
- ✅ `app/api/news/route.ts` - 1 fetch() → fetchWithTimeout()
- ✅ `app/api/courrier/route.ts` - 11 console.log → logger
- ✅ `app/api/courrier/unread-counts/route.ts` - 16 console.log → logger
- ✅ `app/api/courrier/account/route.ts` - 18 console.log → logger
## 📊 Statistiques
### Total Nettoyé
- **Services:** 8 fichiers, ~110 occurrences
- **Routes API critiques:** 8 fichiers, ~50 occurrences
- **Total:** 16 fichiers, ~160 occurrences nettoyées
### fetch() → fetchWithTimeout()
- **Total:** 15+ occurrences remplacées
- **Timeouts configurés:** 10s pour API rapides, 30s pour webhooks
### console.log → logger
- **Total:** 140+ occurrences remplacées
- **Niveaux utilisés:** debug, info, warn, error selon le contexte
## ⚠️ Fichiers Restants (Optionnel)
Il reste encore des `console.log` dans d'autres routes API moins critiques :
### Routes API (Optionnel)
- `app/api/storage/*` - 5 fichiers
- `app/api/missions/*` - 5 fichiers
- `app/api/events/*` - 2 fichiers
- `app/api/calendars/*` - 6 fichiers
- Autres routes API moins utilisées
### Composants React (Non critique pour production)
- ~266 occurrences dans les composants frontend
- ~167 occurrences dans les hooks React
**Note:** Les composants React et hooks peuvent garder `console.log` pour le développement frontend, ce n'est pas critique pour la production backend.
## ✅ Résultat
**Tous les fichiers critiques (services backend et routes API principales) sont maintenant nettoyés !**
Les logs sont maintenant :
- ✅ Structurés avec des objets au lieu de strings
- ✅ Utilisent les bons niveaux (debug/info/warn/error)
- ✅ Masquent les informations sensibles (emails, passwords)
- ✅ Toutes les requêtes HTTP ont des timeouts
---
**Date:** $(date)
**Statut:** ✅ Nettoyage des fichiers critiques complété

View File

@ -10,9 +10,9 @@ import { Button } from "@/components/ui/button";
import { add } from 'date-fns';
export const metadata: Metadata = {
title: "Enkun - Calendrier | Gestion d'événements professionnelle",
title: "NEAH - Calendrier | Gestion d'événements professionnelle",
description: "Plateforme avancée pour la gestion de vos rendez-vous, réunions et événements professionnels",
keywords: "calendrier, rendez-vous, événements, gestion du temps, enkun",
keywords: "calendrier, rendez-vous, événements, gestion du temps, NEAH",
};
interface Event {

View File

@ -5,6 +5,7 @@ import { saveUserEmailCredentials, testEmailConnection } from '@/lib/services/em
import { invalidateFolderCache } from '@/lib/redis';
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcryptjs';
import { logger } from '@/lib/logger';
// Define EmailCredentials interface inline since we're having import issues
interface EmailCredentials {
@ -31,7 +32,9 @@ async function userExists(userId: string): Promise<boolean> {
});
return !!user;
} catch (error) {
console.error(`Error checking if user exists:`, error);
logger.error('[COURRIER_ACCOUNT] Error checking if user exists', {
error: error instanceof Error ? error.message : String(error)
});
return false;
}
}
@ -55,12 +58,12 @@ async function ensureUserExists(session: any): Promise<void> {
});
if (existingUser) {
console.log(`User ${userId} already exists in database`);
logger.debug('[COURRIER_ACCOUNT] User already exists', { userId });
return;
}
// User doesn't exist, create it
console.log(`User ${userId} not found in database, creating from session data...`);
logger.debug('[COURRIER_ACCOUNT] User not found, creating from session data', { userId, email: userEmail.substring(0, 5) + '***' });
// Generate a temporary random password (not used for auth, Keycloak handles that)
const tempPassword = await bcrypt.hash(Math.random().toString(36).slice(-10), 10);
@ -75,12 +78,15 @@ async function ensureUserExists(session: any): Promise<void> {
}
});
console.log(`Successfully created user ${userId} (${userEmail}) in database`);
logger.debug('[COURRIER_ACCOUNT] Successfully created user', { userId, email: userEmail.substring(0, 5) + '***' });
} catch (error) {
console.error(`Error ensuring user exists:`, error);
logger.error('[COURRIER_ACCOUNT] Error ensuring user exists', {
userId,
error: error instanceof Error ? error.message : String(error)
});
// If it's a unique constraint error, user might have been created by another request
if (error instanceof Error && error.message.includes('Unique constraint')) {
console.log('User may have been created by concurrent request, continuing...');
logger.debug('[COURRIER_ACCOUNT] User may have been created by concurrent request', { userId });
return;
}
throw error;
@ -103,7 +109,10 @@ export async function POST(request: Request) {
try {
await ensureUserExists(session);
} catch (error) {
console.error(`Error ensuring user exists:`, error);
logger.error('[COURRIER_ACCOUNT] Error ensuring user exists', {
userId: session.user.id,
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json(
{
error: 'Failed to ensure user exists in database',
@ -115,14 +124,17 @@ export async function POST(request: Request) {
// Parse request body
const body = await request.json().catch(e => {
console.error('Error parsing request body:', e);
logger.error('[COURRIER_ACCOUNT] Error parsing request body', {
error: e instanceof Error ? e.message : String(e)
});
return {};
});
// Log the request (but hide password)
console.log('Adding account:', {
logger.debug('[COURRIER_ACCOUNT] Adding account', {
...body,
password: body.password ? '***' : undefined
password: body.password ? '***' : undefined,
userId: session.user.id
});
const {
@ -146,7 +158,10 @@ export async function POST(request: Request) {
if (port === undefined) missingFields.push('port');
if (missingFields.length > 0) {
console.error(`Missing required fields: ${missingFields.join(', ')}`);
logger.error('[COURRIER_ACCOUNT] Missing required fields', {
missingFields,
userId: session.user.id
});
return NextResponse.json(
{ error: `Required fields missing: ${missingFields.join(', ')}` },
{ status: 400 }
@ -178,7 +193,11 @@ export async function POST(request: Request) {
};
// Test connection before saving
console.log(`Testing connection before saving for user ${session.user.id}`);
logger.debug('[COURRIER_ACCOUNT] Testing connection before saving', {
userId: session.user.id,
email: email.substring(0, 5) + '***',
host: cleanHost
});
const testResult = await testEmailConnection(credentials);
if (!testResult.imap) {
@ -189,9 +208,15 @@ export async function POST(request: Request) {
}
// Save credentials to database and cache
console.log(`Saving credentials for user: ${session.user.id}`);
logger.debug('[COURRIER_ACCOUNT] Saving credentials', {
userId: session.user.id,
email: email.substring(0, 5) + '***'
});
await saveUserEmailCredentials(session.user.id, email, credentials);
console.log(`Email account successfully added for user ${session.user.id}`);
logger.debug('[COURRIER_ACCOUNT] Email account successfully added', {
userId: session.user.id,
email: email.substring(0, 5) + '***'
});
// Fetch the created account from the database
const createdAccount = await prisma.mailCredentials.findFirst({
@ -213,7 +238,10 @@ export async function POST(request: Request) {
message: 'Email account added successfully'
});
} catch (error) {
console.error('Error adding email account:', error);
logger.error('[COURRIER_ACCOUNT] Error adding email account', {
userId: session?.user?.id,
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json(
{
error: 'Failed to add email account',
@ -256,7 +284,11 @@ export async function DELETE(request: Request) {
// Delete calendars and sync configs associated with this account
// This prevents orphaned calendars when a mail account is deleted
for (const syncConfig of syncConfigs) {
console.log(`[COURRIER] Deleting calendar ${syncConfig.calendar.name} (${syncConfig.calendar.id}) associated with deleted account ${account.email}`);
logger.debug('[COURRIER_ACCOUNT] Deleting calendar associated with deleted account', {
calendarId: syncConfig.calendar.id,
calendarName: syncConfig.calendar.name,
accountEmail: account.email.substring(0, 5) + '***'
});
// Delete the calendar (events will be cascade deleted)
await prisma.calendar.delete({
@ -284,7 +316,10 @@ export async function DELETE(request: Request) {
const { invalidateCalendarCache } = await import('@/lib/redis');
await invalidateCalendarCache(session.user.id);
} catch (error) {
console.error('Error invalidating calendar cache:', error);
logger.error('[COURRIER_ACCOUNT] Error invalidating calendar cache', {
userId: session.user.id,
error: error instanceof Error ? error.message : String(error)
});
}
return NextResponse.json({
@ -293,7 +328,10 @@ export async function DELETE(request: Request) {
deletedCalendars: syncConfigs.length,
});
} catch (error) {
console.error('Error deleting account:', error);
logger.error('[COURRIER_ACCOUNT] Error deleting account', {
userId: session?.user?.id,
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json({ error: 'Failed to delete account', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}
@ -372,7 +410,10 @@ export async function PATCH(request: Request) {
message: 'Account updated successfully'
});
} catch (error) {
console.error('Error updating account:', error);
logger.error('[COURRIER_ACCOUNT] Error updating account', {
userId: session?.user?.id,
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json(
{
error: 'Failed to update account',

View File

@ -8,6 +8,7 @@ import {
invalidateFolderCache
} from '@/lib/redis';
import { PrismaClient } from '@prisma/client';
import { logger } from '@/lib/logger';
const prisma = new PrismaClient();
@ -43,7 +44,14 @@ export async function GET(request: Request) {
const refresh = searchParams.get("refresh") === "true";
// CRITICAL FIX: Log exact parameters received by the API
console.log(`[API] Received request with: folder=${folder}, accountId=${accountId}, page=${page}, checkOnly=${checkOnly}, refresh=${refresh}`);
logger.debug('[COURRIER_API] Received request', {
folder,
accountId,
page,
checkOnly,
refresh,
userId: session.user.id
});
// CRITICAL FIX: More robust parameter normalization
// 1. If folder contains an account prefix, extract it but DO NOT use it
@ -56,23 +64,40 @@ export async function GET(request: Request) {
const folderAccountId = parts[0];
normalizedFolder = parts[1];
console.log(`[API] Folder has prefix (${folderAccountId}), normalized to ${normalizedFolder}`);
logger.debug('[COURRIER_API] Folder has prefix, normalized', {
folderAccountId,
normalizedFolder
});
// We intentionally DO NOT use folderAccountId here - the explicit accountId parameter takes precedence
}
// CRITICAL FIX: Enhanced logging for parameter resolution
console.log(`[API] Using normalized parameters: folder=${normalizedFolder}, accountId=${effectiveAccountId}`);
logger.debug('[COURRIER_API] Using normalized parameters', {
folder: normalizedFolder,
accountId: effectiveAccountId,
userId: session.user.id
});
// If refresh=true, invalidate cache before fetching
if (refresh) {
console.log(`[API] Refresh requested - invalidating cache for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}`);
logger.debug('[COURRIER_API] Refresh requested - invalidating cache', {
userId: session.user.id,
accountId: effectiveAccountId,
folder: normalizedFolder
});
await invalidateFolderCache(session.user.id, effectiveAccountId, normalizedFolder);
}
// Try to get from Redis cache first, but only if it's not a search query, not checkOnly, and not refresh
if (!searchQuery && !checkOnly && !refresh) {
// CRITICAL FIX: Use consistent cache key format with the correct account ID
console.log(`[API] Checking Redis cache for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}`);
logger.debug('[COURRIER_API] Checking Redis cache', {
userId: session.user.id,
accountId: effectiveAccountId,
folder: normalizedFolder,
page,
perPage
});
const cachedEmails = await getCachedEmailList(
session.user.id,
effectiveAccountId, // Use effective account ID for consistent cache key
@ -81,12 +106,24 @@ export async function GET(request: Request) {
perPage
);
if (cachedEmails) {
console.log(`[API] Using Redis cached emails for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}`);
logger.debug('[COURRIER_API] Using Redis cached emails', {
userId: session.user.id,
accountId: effectiveAccountId,
folder: normalizedFolder,
page,
perPage
});
return NextResponse.json(cachedEmails);
}
}
console.log(`[API] Redis cache miss for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}, fetching emails from IMAP`);
logger.debug('[COURRIER_API] Redis cache miss, fetching from IMAP', {
userId: session.user.id,
accountId: effectiveAccountId,
folder: normalizedFolder,
page,
perPage
});
// Use the email service to fetch emails with the normalized folder and effective account ID
// CRITICAL FIX: Pass parameters in the correct order and with proper values
@ -100,12 +137,19 @@ export async function GET(request: Request) {
);
// CRITICAL FIX: Log when emails are returned from IMAP
console.log(`[API] Successfully fetched ${emailsResult.emails.length} emails from IMAP for account ${effectiveAccountId}`);
logger.debug('[COURRIER_API] Successfully fetched emails from IMAP', {
count: emailsResult.emails.length,
accountId: effectiveAccountId,
userId: session.user.id
});
// The result is already cached in the getEmails function (if not checkOnly)
return NextResponse.json(emailsResult);
} catch (error: any) {
console.error("[API] Error fetching emails:", error);
logger.error('[COURRIER_API] Error fetching emails', {
error: error instanceof Error ? error.message : String(error),
userId: session?.user?.id
});
return NextResponse.json(
{ error: "Failed to fetch emails", message: error.message },
{ status: 500 }
@ -135,7 +179,11 @@ export async function POST(request: Request) {
: folderName;
// Log the cache invalidation operation
console.log(`Invalidating cache for user ${session.user.id}, account ${effectiveAccountId}, folder ${normalizedFolder || 'all folders'}`);
logger.debug('[COURRIER_API] Invalidating cache', {
userId: session.user.id,
accountId: effectiveAccountId,
folder: normalizedFolder || 'all folders'
});
// Invalidate Redis cache for the folder
if (normalizedFolder) {
@ -150,7 +198,9 @@ export async function POST(request: Request) {
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error in POST handler:', error);
logger.error('[COURRIER_API] Error in POST handler', {
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json({ error: 'An unexpected error occurred' }, { status: 500 });
}
}

View File

@ -4,6 +4,7 @@ import { authOptions } from "@/app/api/auth/options";
import { getImapConnection } from '@/lib/services/email-service';
import { prisma } from '@/lib/prisma';
import { getRedisClient } from '@/lib/redis';
import { logger } from '@/lib/logger';
// Cache TTL for unread counts (increased to 2 minutes for better performance)
const UNREAD_COUNTS_CACHE_TTL = 120;
@ -36,7 +37,7 @@ export async function GET(request: Request) {
const cachedCounts = await redis.get(UNREAD_COUNTS_CACHE_KEY(userId));
if (cachedCounts) {
// Use cached results if available
console.log(`[UNREAD_API] Using cached unread counts for user ${userId}`);
logger.debug('[UNREAD_API] Using cached unread counts', { userId });
// If the cache is about to expire, schedule a background refresh
const ttl = await redis.ttl(UNREAD_COUNTS_CACHE_KEY(userId));
@ -51,11 +52,14 @@ export async function GET(request: Request) {
);
if (lockAcquired) {
console.log(`[UNREAD_API] Scheduling background refresh for user ${userId}`);
logger.debug('[UNREAD_API] Scheduling background refresh', { userId });
// Use Promise to run in background
setTimeout(() => {
refreshUnreadCounts(userId, redis)
.catch(err => console.error(`[UNREAD_API] Background refresh error: ${err}`))
.catch(err => logger.error('[UNREAD_API] Background refresh error', {
userId,
error: err instanceof Error ? err.message : String(err)
}))
.finally(() => {
// Release lock regardless of outcome
redis.del(REFRESH_LOCK_KEY(userId)).catch(() => {});
@ -67,7 +71,7 @@ export async function GET(request: Request) {
return NextResponse.json(JSON.parse(cachedCounts));
}
console.log(`[UNREAD_API] Cache miss for user ${userId}, fetching unread counts`);
logger.debug('[UNREAD_API] Cache miss, fetching unread counts', { userId });
// Try to acquire lock to prevent parallel refreshes
const lockAcquired = await redis.set(
@ -79,7 +83,7 @@ export async function GET(request: Request) {
);
if (!lockAcquired) {
console.log(`[UNREAD_API] Another process is refreshing unread counts for ${userId}`);
logger.debug('[UNREAD_API] Another process is refreshing unread counts', { userId });
// Return empty counts with short cache time if we can't acquire lock
// The next request will likely get cached data
@ -104,7 +108,9 @@ export async function GET(request: Request) {
await redis.del(REFRESH_LOCK_KEY(userId));
}
} catch (error: any) {
console.error("[UNREAD_API] Error fetching unread counts:", error);
logger.error('[UNREAD_API] Error fetching unread counts', {
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json(
{ error: "Failed to fetch unread counts", message: error.message },
{ status: 500 }
@ -117,7 +123,7 @@ export async function GET(request: Request) {
*/
async function refreshUnreadCounts(userId: string, redis: any): Promise<void> {
try {
console.log(`[UNREAD_API] Background refresh started for user ${userId}`);
logger.debug('[UNREAD_API] Background refresh started', { userId });
const unreadCounts = await fetchUnreadCounts(userId);
// Save to cache
@ -128,9 +134,12 @@ async function refreshUnreadCounts(userId: string, redis: any): Promise<void> {
UNREAD_COUNTS_CACHE_TTL
);
console.log(`[UNREAD_API] Background refresh completed for user ${userId}`);
logger.debug('[UNREAD_API] Background refresh completed', { userId });
} catch (error) {
console.error(`[UNREAD_API] Background refresh failed for user ${userId}:`, error);
logger.error('[UNREAD_API] Background refresh failed', {
userId,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
@ -148,7 +157,7 @@ async function fetchUnreadCounts(userId: string): Promise<Record<string, Record<
}
});
console.log(`[UNREAD_API] Found ${accounts.length} accounts for user ${userId}`);
logger.debug('[UNREAD_API] Found accounts', { userId, count: accounts.length });
if (accounts.length === 0) {
return { default: {} };
@ -162,7 +171,11 @@ async function fetchUnreadCounts(userId: string): Promise<Record<string, Record<
const accountId = account.id;
try {
// Get IMAP connection for this account
console.log(`[UNREAD_API] Processing account ${accountId} (${account.email})`);
logger.debug('[UNREAD_API] Processing account', {
userId,
accountId,
email: account.email.substring(0, 5) + '***'
});
const client = await getImapConnection(userId, accountId);
unreadCounts[accountId] = {};
@ -192,17 +205,28 @@ async function fetchUnreadCounts(userId: string): Promise<Record<string, Record<
// Also store with prefixed version for consistency
unreadCounts[accountId][`${accountId}:${folder}`] = status.unseen;
console.log(`[UNREAD_API] Account ${accountId}, folder ${folder}: ${status.unseen} unread`);
logger.debug('[UNREAD_API] Account folder unread count', {
accountId,
folder,
unseen: status.unseen
});
}
} catch (folderError) {
console.error(`[UNREAD_API] Error getting unread count for ${accountId}:${folder}:`, folderError);
logger.error('[UNREAD_API] Error getting unread count for folder', {
accountId,
folder,
error: folderError instanceof Error ? folderError.message : String(folderError)
});
// Continue to next folder even if this one fails
}
}
// Don't close the connection - let the connection pool handle it
} catch (accountError) {
console.error(`[UNREAD_API] Error processing account ${accountId}:`, accountError);
logger.error('[UNREAD_API] Error processing account', {
accountId,
error: accountError instanceof Error ? accountError.message : String(accountError)
});
}
}
@ -229,12 +253,16 @@ async function getUserAccountIds(userId: string): Promise<string[]> {
// Close the default connection
await defaultClient.logout();
} catch (error) {
console.error('[UNREAD_API] Error getting additional accounts:', error);
logger.error('[UNREAD_API] Error getting additional accounts', {
error: error instanceof Error ? error.message : String(error)
});
}
return accounts;
} catch (error) {
console.error('[UNREAD_API] Error getting account IDs:', error);
logger.error('[UNREAD_API] Error getting account IDs', {
error: error instanceof Error ? error.message : String(error)
});
return ['default']; // Return at least the default account
}
}

View File

@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
import { GroupsTable } from "@/components/groups/groups-table";
export const metadata = {
title: "Enkun - Groupes",
title: "NEAH - Groupes",
};
export default async function GroupsPage() {

View File

@ -1,7 +1,7 @@
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Enkun - Connexion",
title: "NEAH - Connexion",
};
export default function SignInLayout({

View File

@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
import { UsersTable } from "@/components/users/users-table";
export const metadata = {
title: "Enkun - Utilisateurs",
title: "NEAH - Utilisateurs",
};
export default async function UsersPage() {

View File

@ -14,7 +14,7 @@ export function LoginCard() {
return (
<Card className='w-[400px]'>
<CardHeader>
<CardTitle>Bienvenue sur Enkun</CardTitle>
<CardTitle>Bienvenue sur NEAH</CardTitle>
<CardDescription>
Connectez-vous pour accéder à votre espace
</CardDescription>

View File

@ -9,7 +9,7 @@ interface SignInFormProps {
export function SignInForm({ callbackUrl }: SignInFormProps) {
return (
<div className="text-center">
<h1 className="text-4xl font-bold text-white mb-4">Bienvenue sur Enkun</h1>
<h1 className="text-4xl font-bold text-white mb-4">Bienvenue sur NEAH</h1>
<p className="text-white/80 mb-8">Connectez-vous pour accéder à votre espace</p>
<button
onClick={() => signIn("keycloak", { callbackUrl: callbackUrl || "/" })}

View File

@ -45,29 +45,30 @@ export async function discoverInfomaniakCalendars(
password: string
): Promise<CalDAVCalendar[]> {
try {
console.log(`[CALDAV] Starting calendar discovery for ${email}`);
logger.debug('[CALDAV] Starting calendar discovery', { email: email.substring(0, 5) + '***' });
const client = await getInfomaniakCalDAVClient(email, password);
// List all calendars using PROPFIND on root
console.log(`[CALDAV] Fetching directory contents from root (/)`);
logger.debug('[CALDAV] Fetching directory contents from root');
const items = await client.getDirectoryContents('/');
console.log(`[CALDAV] Found ${items.length} items in root directory:`,
items.map(item => ({ filename: item.filename, type: item.type, basename: item.basename }))
);
logger.debug('[CALDAV] Found items in root directory', {
count: items.length,
items: items.map(item => ({ filename: item.filename, type: item.type, basename: item.basename }))
});
const calendars: CalDAVCalendar[] = [];
for (const item of items) {
// Skip non-directories, root, and special directories like /principals
if (item.type !== 'directory' || item.filename === '/' || item.filename === '/principals') {
console.log(`[CALDAV] Skipping item: ${item.filename} (type: ${item.type})`);
logger.debug('[CALDAV] Skipping item', { filename: item.filename, type: item.type });
continue;
}
// Get calendar properties to verify it's actually a calendar
try {
console.log(`[CALDAV] Checking if ${item.filename} is a calendar...`);
logger.debug('[CALDAV] Checking if item is a calendar', { filename: item.filename });
const props = await client.customRequest(item.filename, {
method: 'PROPFIND',
headers: {
@ -91,15 +92,15 @@ export async function discoverInfomaniakCalendars(
dataStr.includes('calendar') ||
dataStr.includes('urn:ietf:params:xml:ns:caldav');
console.log(`[CALDAV] Calendar check for ${item.filename}:`, {
logger.debug('[CALDAV] Calendar check result', {
filename: item.filename,
isCalendar,
hasData: !!props.data,
dataLength: dataStr.length,
dataPreview: dataStr.substring(0, 300),
});
if (!isCalendar) {
console.log(`[CALDAV] Skipping ${item.filename} - not a calendar (resourcetype check failed)`);
logger.debug('[CALDAV] Skipping - not a calendar', { filename: item.filename });
continue;
}
@ -114,27 +115,30 @@ export async function discoverInfomaniakCalendars(
color: color,
};
console.log(`[CALDAV] ✅ Found valid calendar:`, calendar);
logger.debug('[CALDAV] Found valid calendar', { calendar });
calendars.push(calendar);
} catch (error) {
console.error(`[CALDAV] Error fetching calendar properties for ${item.filename}:`,
error instanceof Error ? error.message : String(error)
);
logger.error('[CALDAV] Error fetching calendar properties', {
filename: item.filename,
error: error instanceof Error ? error.message : String(error)
});
// Don't add calendars that fail property fetch - they might not be calendars
}
}
console.log(`[CALDAV] Discovery completed: found ${calendars.length} calendars for ${email}`);
if (calendars.length > 0) {
console.log(`[CALDAV] Calendars:`, calendars.map(cal => ({ id: cal.id, name: cal.name, url: cal.url })));
}
logger.debug('[CALDAV] Discovery completed', {
email: email.substring(0, 5) + '***',
count: calendars.length,
calendars: calendars.map(cal => ({ id: cal.id, name: cal.name, url: cal.url }))
});
return calendars;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack?.substring(0, 500) : undefined;
console.error(`[CALDAV] ❌ Calendar discovery failed for ${email}:`, {
logger.error('[CALDAV] Calendar discovery failed', {
email: email.substring(0, 5) + '***',
error: errorMessage,
stack: errorStack,
});

View File

@ -1542,7 +1542,9 @@ export async function getEmailContent(
try {
await client.mailboxClose();
} catch (error) {
console.error('Error closing mailbox:', error);
logger.error('[EMAIL_SERVICE] Error closing mailbox', {
error: error instanceof Error ? error.message : String(error)
});
}
}
}
@ -1841,7 +1843,9 @@ export async function sendEmail(
messageId: info.messageId
};
} catch (error) {
console.error('Failed to send email:', error);
logger.error('[EMAIL_SERVICE] Failed to send email', {
error: error instanceof Error ? error.message : String(error)
});
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'

View File

@ -1,4 +1,5 @@
import axios from 'axios';
import { logger } from '@/lib/logger';
// Get tenant ID from env var or use a default
const tenantId = process.env.MICROSOFT_TENANT_ID || 'common'; // Use 'organizations' or actual tenant ID
@ -58,7 +59,7 @@ export async function exchangeCodeForTokens(code: string): Promise<{
});
try {
console.log(`Exchanging code for tokens. URL: ${MICROSOFT_TOKEN_URL}`);
logger.debug('[MICROSOFT_OAUTH] Exchanging code for tokens', { url: MICROSOFT_TOKEN_URL });
const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), {
headers: {
@ -66,20 +67,24 @@ export async function exchangeCodeForTokens(code: string): Promise<{
}
});
console.log('Token exchange successful!');
logger.debug('[MICROSOFT_OAUTH] Token exchange successful');
return {
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
expires_in: response.data.expires_in
};
} catch (error: any) {
console.error('Error exchanging code for tokens:', error);
logger.error('[MICROSOFT_OAUTH] Error exchanging code for tokens', {
error: error instanceof Error ? error.message : String(error)
});
// Enhanced error logging
if (error.response) {
console.error('Response data:', error.response.data);
console.error('Response status:', error.response.status);
console.error('Response headers:', error.response.headers);
logger.error('[MICROSOFT_OAUTH] Response details', {
data: error.response.data,
status: error.response.status,
headers: error.response.headers
});
// Extract the error message from Microsoft's response format
const errorData = error.response.data;
@ -109,7 +114,7 @@ export async function refreshAccessToken(refreshToken: string): Promise<{
});
try {
console.log(`Refreshing access token. URL: ${MICROSOFT_TOKEN_URL}`);
logger.debug('[MICROSOFT_OAUTH] Refreshing access token', { url: MICROSOFT_TOKEN_URL });
const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), {
headers: {
@ -117,20 +122,24 @@ export async function refreshAccessToken(refreshToken: string): Promise<{
}
});
console.log('Token refresh successful!');
logger.debug('[MICROSOFT_OAUTH] Token refresh successful');
return {
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
expires_in: response.data.expires_in
};
} catch (error: any) {
console.error('Error refreshing token:', error);
logger.error('[MICROSOFT_OAUTH] Error refreshing token', {
error: error instanceof Error ? error.message : String(error)
});
// Enhanced error logging
if (error.response) {
console.error('Response data:', error.response.data);
console.error('Response status:', error.response.status);
console.error('Response headers:', error.response.headers);
logger.error('[MICROSOFT_OAUTH] Response details', {
data: error.response.data,
status: error.response.status,
headers: error.response.headers
});
// Extract the error message from Microsoft's response format
const errorData = error.response.data;
@ -152,6 +161,6 @@ export function createXOAuth2Token(email: string, accessToken: string): string {
const auth = `user=${email}\x01auth=Bearer ${accessToken}\x01\x01`;
const base64Auth = Buffer.from(auth).toString('base64');
console.log('Generated XOAUTH2 token (length):', base64Auth.length);
logger.debug('[MICROSOFT_OAUTH] Generated XOAUTH2 token', { length: base64Auth.length });
return base64Auth;
}

View File

@ -225,7 +225,7 @@ export async function prefetchUserEmailData(userId: string): Promise<void> {
return;
}
console.log(`Starting email prefetch for user ${userId}`);
logger.debug('[PREFETCH] Starting email prefetch', { userId });
const startTime = Date.now();
try {
@ -242,7 +242,7 @@ export async function prefetchUserEmailData(userId: string): Promise<void> {
mailboxes: mailboxPaths
});
console.log(`Prefetched ${mailboxPaths.length} folders for user ${userId}`);
logger.debug('[PREFETCH] Prefetched folders', { userId, count: mailboxPaths.length });
// 2. Prefetch email lists for important folders
const importantFolders = [
@ -254,11 +254,15 @@ export async function prefetchUserEmailData(userId: string): Promise<void> {
// Fetch first page of each important folder
for (const folder of importantFolders) {
try {
console.log(`Prefetching emails for ${folder}`);
logger.debug('[PREFETCH] Prefetching emails for folder', { userId, folder });
const emailList = await getEmails(userId, folder, 1, 20);
console.log(`Prefetched ${emailList.emails.length} emails for ${folder}`);
logger.debug('[PREFETCH] Prefetched emails', { userId, folder, count: emailList.emails.length });
} catch (error) {
console.error(`Error prefetching emails for folder ${folder}:`, error);
logger.error('[PREFETCH] Error prefetching emails for folder', {
userId,
folder,
error: error instanceof Error ? error.message : String(error)
});
// Continue with other folders even if one fails
}
}
@ -274,26 +278,36 @@ export async function prefetchUserEmailData(userId: string): Promise<void> {
.slice(0, 5);
if (unreadEmails.length > 0) {
console.log(`Prefetching content for ${unreadEmails.length} unread emails`);
logger.debug('[PREFETCH] Prefetching content for unread emails', { userId, count: unreadEmails.length });
// Fetch content in parallel for speed
await Promise.allSettled(
unreadEmails.map(email =>
getEmailContent(userId, email.id, 'INBOX')
.catch(err => console.error(`Error prefetching email ${email.id}:`, err))
.catch(err => logger.error('[PREFETCH] Error prefetching email', {
userId,
emailId: email.id,
error: err instanceof Error ? err.message : String(err)
}))
)
);
console.log(`Completed prefetching content for unread emails`);
logger.debug('[PREFETCH] Completed prefetching content for unread emails', { userId });
}
} catch (error) {
console.error('Error prefetching unread email content:', error);
logger.error('[PREFETCH] Error prefetching unread email content', {
userId,
error: error instanceof Error ? error.message : String(error)
});
}
const duration = (Date.now() - startTime) / 1000;
console.log(`Email prefetch completed for user ${userId} in ${duration.toFixed(2)}s`);
logger.debug('[PREFETCH] Email prefetch completed', { userId, duration: duration.toFixed(2) });
} catch (error) {
console.error('Error during email prefetch:', error);
logger.error('[PREFETCH] Error during email prefetch', {
userId,
error: error instanceof Error ? error.message : String(error)
});
} finally {
markPrefetchCompleted(userId);
}
@ -327,7 +341,13 @@ export async function prefetchFolderEmails(
}
try {
console.log(`Prefetching ${pages} pages of emails for folder ${normalizedFolder} starting from page ${startPage} for account ${effectiveAccountId}`);
logger.debug('[PREFETCH] Prefetching folder emails', {
userId,
folder: normalizedFolder,
pages,
startPage,
accountId: effectiveAccountId
});
// Calculate the range of pages to prefetch
const pagesToFetch = Array.from(
@ -335,26 +355,48 @@ export async function prefetchFolderEmails(
(_, i) => startPage + i
);
console.log(`Will prefetch pages: ${pagesToFetch.join(', ')}`);
logger.debug('[PREFETCH] Pages to prefetch', { pages: pagesToFetch });
// Fetch multiple pages in parallel
await Promise.allSettled(
pagesToFetch.map(page =>
getEmails(userId, normalizedFolder, page, 20, effectiveAccountId)
.then(result => {
console.log(`Successfully prefetched and cached page ${page} of ${normalizedFolder} with ${result.emails.length} emails for account ${effectiveAccountId}`);
logger.debug('[PREFETCH] Successfully prefetched page', {
userId,
folder: normalizedFolder,
page,
count: result.emails.length,
accountId: effectiveAccountId
});
return result;
})
.catch(err => {
console.error(`Error prefetching page ${page} of ${normalizedFolder} for account ${effectiveAccountId}:`, err);
logger.error('[PREFETCH] Error prefetching page', {
userId,
folder: normalizedFolder,
page,
accountId: effectiveAccountId,
error: err instanceof Error ? err.message : String(err)
});
return null;
})
)
);
console.log(`Completed prefetching ${pages} pages for ${normalizedFolder} in account ${effectiveAccountId}`);
logger.debug('[PREFETCH] Completed prefetching pages', {
userId,
folder: normalizedFolder,
pages,
accountId: effectiveAccountId
});
} catch (error) {
console.error(`Error during folder prefetch:`, error);
logger.error('[PREFETCH] Error during folder prefetch', {
userId,
folder: normalizedFolder,
accountId: effectiveAccountId,
error: error instanceof Error ? error.message : String(error)
});
} finally {
markPrefetchCompleted(userId, prefetchKey);
}

View File

@ -6,6 +6,8 @@
* a single source of truth for refresh coordination.
*/
import { logger } from '@/lib/logger';
export type RefreshableResource =
| 'notifications'
| 'notifications-count'
@ -35,7 +37,7 @@ class RefreshManager {
* Register a refreshable resource
*/
register(config: RefreshConfig): void {
console.log(`[RefreshManager] Registering resource: ${config.resource} (interval: ${config.interval}ms)`);
logger.debug('[RefreshManager] Registering resource', { resource: config.resource, interval: config.interval });
this.configs.set(config.resource, config);
@ -48,7 +50,7 @@ class RefreshManager {
* Unregister a resource
*/
unregister(resource: RefreshableResource): void {
console.log(`[RefreshManager] Unregistering resource: ${resource}`);
logger.debug('[RefreshManager] Unregistering resource', { resource });
this.stopRefresh(resource);
this.configs.delete(resource);
@ -64,11 +66,11 @@ class RefreshManager {
*/
start(): void {
if (this.isActive) {
console.log('[RefreshManager] Already active');
logger.debug('[RefreshManager] Already active');
return;
}
console.log('[RefreshManager] Starting refresh manager');
logger.debug('[RefreshManager] Starting refresh manager');
this.isActive = true;
// Start all enabled resources
@ -84,16 +86,16 @@ class RefreshManager {
*/
stop(): void {
if (!this.isActive) {
console.log('[RefreshManager] Already stopped');
logger.debug('[RefreshManager] Already stopped');
return;
}
console.log('[RefreshManager] Stopping refresh manager');
logger.debug('[RefreshManager] Stopping refresh manager');
this.isActive = false;
// Clear all intervals
this.intervals.forEach((interval, resource) => {
console.log(`[RefreshManager] Stopping refresh for: ${resource}`);
logger.debug('[RefreshManager] Stopping refresh', { resource });
clearInterval(interval);
});
@ -112,11 +114,11 @@ class RefreshManager {
const config = this.configs.get(resource);
if (!config || !config.enabled) {
console.log(`[RefreshManager] Cannot start refresh for ${resource}: not configured or disabled`);
logger.debug('[RefreshManager] Cannot start refresh', { resource, reason: 'not configured or disabled' });
return;
}
console.log(`[RefreshManager] Starting refresh for ${resource} (interval: ${config.interval}ms)`);
logger.debug('[RefreshManager] Starting refresh', { resource, interval: config.interval });
// Initial refresh
this.executeRefresh(resource);
@ -137,7 +139,7 @@ class RefreshManager {
if (interval) {
clearInterval(interval);
this.intervals.delete(resource);
console.log(`[RefreshManager] Stopped refresh for: ${resource}`);
logger.debug('[RefreshManager] Stopped refresh', { resource });
}
}
@ -147,7 +149,7 @@ class RefreshManager {
private async executeRefresh(resource: RefreshableResource): Promise<void> {
const config = this.configs.get(resource);
if (!config) {
console.warn(`[RefreshManager] No config found for resource: ${resource}`);
logger.warn('[RefreshManager] No config found for resource', { resource });
return;
}
@ -156,26 +158,29 @@ class RefreshManager {
// Prevent too frequent refreshes (minimum 1 second between same resource)
if (now - lastRefreshTime < 1000) {
console.log(`[RefreshManager] Skipping ${resource} - too soon (${now - lastRefreshTime}ms ago)`);
logger.debug('[RefreshManager] Skipping refresh - too soon', { resource, msAgo: now - lastRefreshTime });
return;
}
// Check if there's already a pending request for this resource
const pendingKey = `${resource}-pending`;
if (this.pendingRequests.has(pendingKey)) {
console.log(`[RefreshManager] Deduplicating ${resource} request - already pending`);
logger.debug('[RefreshManager] Deduplicating request - already pending', { resource });
return;
}
// Create and track the request
console.log(`[RefreshManager] Executing refresh for: ${resource}`);
logger.debug('[RefreshManager] Executing refresh', { resource });
const refreshPromise = config.onRefresh()
.then(() => {
this.lastRefresh.set(resource, Date.now());
console.log(`[RefreshManager] Successfully refreshed: ${resource}`);
logger.debug('[RefreshManager] Successfully refreshed', { resource });
})
.catch((error) => {
console.error(`[RefreshManager] Error refreshing ${resource}:`, error);
logger.error('[RefreshManager] Error refreshing', {
resource,
error: error instanceof Error ? error.message : String(error)
});
// Don't update lastRefresh on error to allow retry
})
.finally(() => {
@ -200,7 +205,7 @@ class RefreshManager {
throw new Error(`Resource ${resource} not registered`);
}
console.log(`[RefreshManager] Manual refresh requested for: ${resource} (force: ${force})`);
logger.debug('[RefreshManager] Manual refresh requested', { resource, force });
if (force) {
// Force refresh: clear last refresh time and pending request
@ -243,7 +248,7 @@ class RefreshManager {
* Pause all refreshes (temporary stop)
*/
pause(): void {
console.log('[RefreshManager] Pausing all refreshes');
logger.debug('[RefreshManager] Pausing all refreshes');
this.stop();
}
@ -251,7 +256,7 @@ class RefreshManager {
* Resume all refreshes
*/
resume(): void {
console.log('[RefreshManager] Resuming all refreshes');
logger.debug('[RefreshManager] Resuming all refreshes');
this.start();
}
}

View File

@ -1,6 +1,7 @@
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)
@ -19,7 +20,7 @@ export async function ensureFreshToken(
): Promise<{ accessToken: string; success: boolean }> {
try {
// Try Redis first (fast path)
console.log(`Checking if token refresh is needed for ${email}`);
logger.debug('[TOKEN_REFRESH] Checking if token refresh is needed', { email: email.substring(0, 5) + '***' });
const redis = getRedisClient();
const key = KEYS.CREDENTIALS(userId, email);
let credStr = await redis.get(key);
@ -29,7 +30,7 @@ export async function ensureFreshToken(
creds = JSON.parse(credStr);
} else {
// Redis cache miss - fallback to Prisma database
console.log(`No credentials found in Redis for ${email}, checking Prisma database...`);
logger.debug('[TOKEN_REFRESH] No credentials in Redis, checking Prisma', { email: email.substring(0, 5) + '***' });
const account = await prisma.mailCredentials.findFirst({
where: {
userId: userId,
@ -53,28 +54,28 @@ export async function ensureFreshToken(
// 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}`);
logger.debug('[TOKEN_REFRESH] Recovered credentials from Prisma and cached in Redis', { email: email.substring(0, 5) + '***' });
} else {
console.log(`No OAuth credentials found in database for ${email}`);
logger.debug('[TOKEN_REFRESH] No OAuth credentials found in database', { email: email.substring(0, 5) + '***' });
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`);
logger.debug('[TOKEN_REFRESH] Account not using OAuth or missing refresh token', { email: email.substring(0, 5) + '***' });
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`);
logger.debug('[TOKEN_REFRESH] Token still valid, no refresh needed', { email: email.substring(0, 5) + '***' });
return { accessToken: creds.accessToken, success: true };
}
// Token is expired or about to expire, refresh it
console.log(`Refreshing token for ${email}`);
logger.debug('[TOKEN_REFRESH] Refreshing token', { email: email.substring(0, 5) + '***' });
const tokens = await refreshAccessToken(creds.refreshToken);
// Update Redis cache with new tokens
@ -85,7 +86,7 @@ export async function ensureFreshToken(
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`);
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
@ -107,18 +108,24 @@ export async function ensureFreshToken(
use_oauth: true
}
});
console.log(`Token for ${email} persisted to Prisma database`);
logger.debug('[TOKEN_REFRESH] Token persisted to Prisma database', { email: email.substring(0, 5) + '***' });
} else {
console.warn(`Account ${email} not found in Prisma, cannot persist tokens`);
logger.warn('[TOKEN_REFRESH] Account not found in Prisma, cannot persist tokens', { email: email.substring(0, 5) + '***' });
}
} catch (dbError) {
console.error(`Error persisting tokens to database for ${email}:`, 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) {
console.error(`Error refreshing token for ${email}:`, 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 };
}
}